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

View File

@ -1,25 +1,22 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
// process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function(config) {
config.set({
const { join } = require('path');
const { constants } = require('karma');
module.exports = () => {
return {
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('karma-coverage'),
require('karma-mocha-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
files: [
{
pattern:
'./node_modules/@angular/material/prebuilt-themes/indigo-pink.css',
watched: false
},
{
pattern:
'./node_modules/@alfresco/adf-core/bundles/assets/adf-core/i18n/en.json',
@ -46,18 +43,36 @@ module.exports = function(config) {
'/base/node_modules/@alfresco/adf-content-services/bundles/assets/adf-content-services/i18n/en.json'
},
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, 'coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter: {
dir: join(__dirname, './coverage'),
subdir: '.',
reporters: [{ type: 'html' }, { type: 'text-summary' }],
check: {
global: {
statements: 75,
branches: 67,
functions: 73,
lines: 75
}
}
},
reporters: ['mocha', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
logLevel: constants.LOG_INFO,
autoWatch: true,
browsers: ['ChromeHeadless'],
customLaunchers: {
@ -65,19 +80,15 @@ module.exports = function(config) {
base: 'Chrome',
flags: [
'--no-sandbox',
// '--headless',
'--headless',
'--disable-gpu',
'--remote-debugging-port=9222'
]
}
},
singleRun: true,
captureTimeout: 180000,
browserDisconnectTimeout: 180000,
browserDisconnectTolerance: 3,
browserNoActivityTimeout: 300000,
restartOnFileChange: true,
// workaround for alfresco-js-api builds
webpack: { node: { fs: 'empty' } }
});
};
};

58
package-lock.json generated
View File

@ -12916,6 +12916,64 @@
"which": "^1.2.1"
}
},
"karma-coverage": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.0.tgz",
"integrity": "sha512-gPVdoZBNDZ08UCzdMHHhEImKrw1+PAOQOIiffv1YsvxFhBjqvo/SVXNk4tqn1SYqX0BJZT6S/59zgxiBe+9OuA==",
"dev": true,
"requires": {
"istanbul-lib-coverage": "^3.2.0",
"istanbul-lib-instrument": "^5.1.0",
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^4.0.1",
"istanbul-reports": "^3.0.5",
"minimatch": "^3.0.4"
},
"dependencies": {
"istanbul-lib-coverage": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
"integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
"dev": true
},
"istanbul-lib-source-maps": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
"integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"istanbul-lib-coverage": "^3.0.0",
"source-map": "^0.6.1"
}
},
"istanbul-reports": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz",
"integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==",
"dev": true,
"requires": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"karma-coverage-istanbul-reporter": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz",

View File

@ -16,7 +16,6 @@
"build.release": "npm run build -- --configuration=production,release",
"build-libs": "ng build aca-shared && ng build adf-office-services-ext && ng build aca-about && ng build aca-viewer && ng build aca-preview && ng build aca-folder-rules && ng build aca-content",
"test": "ng test",
"test:ci": "ng test adf-office-services-ext && ng test content-ce --code-coverage",
"lint": "NODE_OPTIONS=--max_old_space_size=4096 ng lint",
"update-webdriver": "./scripts/update-webdriver.sh",
"e2e": "npm run update-webdriver && protractor $SUITE",
@ -102,6 +101,7 @@
"jasmine-spec-reporter": "~5.0.0",
"karma": "^6.4.1",
"karma-chrome-launcher": "~3.1.1",
"karma-coverage": "^2.2.0",
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",

View File

@ -1,32 +1,15 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const { join } = require('path');
const getBaseKarmaConfig = require('../../karma.conf');
module.exports = function (config) {
const baseConfig = getBaseKarmaConfig();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
...baseConfig,
coverageReporter: {
...baseConfig.coverageReporter,
dir: join(__dirname, '../../coverage/aca-about'),
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/aca-about'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: true,
restartOnFileChange: true
});
};

View File

@ -1,32 +1,15 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const { join } = require('path');
const getBaseKarmaConfig = require('../../karma.conf');
module.exports = function (config) {
const baseConfig = getBaseKarmaConfig();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
...baseConfig,
coverageReporter: {
...baseConfig.coverageReporter,
dir: join(__dirname, '../../coverage/aca-content'),
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/aca-content'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: true,
restartOnFileChange: true
});
};

View File

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

View File

@ -1,32 +1,15 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const { join } = require('path');
const getBaseKarmaConfig = require('../../karma.conf');
module.exports = function (config) {
const baseConfig = getBaseKarmaConfig();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
...baseConfig,
coverageReporter: {
...baseConfig.coverageReporter,
dir: join(__dirname, '../../coverage/aca-folder-rules'),
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/aca-folder-rules'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: true,
restartOnFileChange: true
});
};

View File

@ -1,32 +1,15 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const { join } = require('path');
const getBaseKarmaConfig = require('../../karma.conf');
module.exports = function (config) {
const baseConfig = getBaseKarmaConfig();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
...baseConfig,
coverageReporter: {
...baseConfig.coverageReporter,
dir: join(__dirname, '../../coverage/aca-preview'),
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/aca-preview'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: true,
restartOnFileChange: true
});
};

View File

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

View File

@ -1,33 +1,15 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const { join } = require('path');
const getBaseKarmaConfig = require('../../karma.conf');
module.exports = function (config) {
const baseConfig = getBaseKarmaConfig();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('karma-mocha-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
...baseConfig,
coverageReporter: {
...baseConfig.coverageReporter,
dir: join(__dirname, '../../coverage/aca-shared'),
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/aca-shared'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['mocha', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: true,
restartOnFileChange: true
});
};

View File

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

View File

@ -38,6 +38,7 @@ import { HttpClientModule } from '@angular/common/http';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { EffectsModule } from '@ngrx/effects';
import { Subscription } from 'rxjs';
export const INITIAL_APP_STATE: AppState = {
appName: 'Alfresco Content Application',
@ -98,6 +99,14 @@ class TestComponent extends PageComponent {
constructor(store: Store<AppStore>, extensions: AppExtensionService, content: DocumentBasePageService) {
super(store, extensions, content);
}
addSubscription(entry: Subscription) {
this.subscriptions.push(entry);
}
getSubscriptions(): Subscription[] {
return this.subscriptions;
}
}
describe('PageComponent', () => {
@ -341,4 +350,66 @@ describe('Info Drawer state', () => {
}
});
});
it('should not resolve custom image', () => {
expect(component.imageResolver(null)).toBe(null);
});
it('should resolve custom image for locked node', () => {
const row: any = {
node: {
entry: {
isLocked: true
}
}
};
expect(component.imageResolver(row)).toBe('assets/images/baseline-lock-24px.svg');
});
it('should resolve custom image for a library', () => {
const row: any = {
node: {
entry: {
nodeType: 'st:site'
}
}
};
expect(component.imageResolver(row)).toBe('assets/images/baseline-library_books-24px.svg');
});
it('should track elements by action id ', () => {
const action: any = { id: 'action1' };
expect(component.trackByActionId(0, action)).toBe('action1');
});
it('should track elements by id ', () => {
const action: any = { id: 'action1' };
expect(component.trackById(0, action)).toBe('action1');
});
it('should track elements by column id ', () => {
const action: any = { id: 'action1' };
expect(component.trackByColumnId(0, action)).toBe('action1');
});
it('should cleanup subscriptions on destroy', () => {
const sub = jasmine.createSpyObj('sub', ['unsubscribe']);
expect(component.getSubscriptions().length).toBe(0);
component.addSubscription(sub);
expect(component.getSubscriptions().length).toBe(1);
component.ngOnDestroy();
expect(sub.unsubscribe).toHaveBeenCalled();
expect(component.getSubscriptions().length).toBe(0);
});
it('should update filter sorting', () => {
const event = new CustomEvent('sorting-changed', { detail: { key: 'name', direction: 'asc' } });
component.onSortingChanged(event);
expect(component.filterSorting).toBe('name-asc');
});
});

View File

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

View File

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

View File

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

View File

@ -23,10 +23,51 @@
* 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 { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { TranslateModule } from '@ngx-translate/core';
import { ToolbarButtonType } from '../toolbar-button/toolbar-button.component';
import { ChangeDetectorRef } from '@angular/core';
import { ContentActionType } from '@alfresco/adf-extensions';
import { IconModule } from '@alfresco/adf-core';
describe('ToolbarActionComponent', () => {
it('should be defined', () => {
expect(ToolbarActionComponent).toBeDefined();
let fixture: ComponentFixture<ToolbarActionComponent>;
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/>.
*/
import { ToolbarButtonComponent } from './toolbar-button.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToolbarButtonComponent, ToolbarButtonType } from './toolbar-button.component';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { TranslateModule } from '@ngx-translate/core';
import { IconModule, TranslationMock, TranslationService } from '@alfresco/adf-core';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { AppExtensionService } from '../../../services/app.extension.service';
import { ContentActionType } from '@alfresco/adf-extensions';
import { MatButtonModule } from '@angular/material/button';
describe('ToolbarButtonComponent', () => {
it('should be defined', () => {
expect(ToolbarButtonComponent).toBeDefined();
let fixture: ComponentFixture<ToolbarButtonComponent>;
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/>.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToolbarMenuItemComponent } from './toolbar-menu-item.component';
import { AppExtensionService } from '../../../services/app.extension.service';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { TranslateModule } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import { IconModule, TranslationMock, TranslationService } from '@alfresco/adf-core';
import { MatButtonModule } from '@angular/material/button';
import { of } from 'rxjs';
import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions';
describe('ToolbarMenuItemComponent', () => {
it('should be defined', () => {
expect(ToolbarMenuItemComponent).toBeDefined();
let fixture: ComponentFixture<ToolbarMenuItemComponent>;
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 { OverlayModule } from '@angular/cdk/overlay';
import { TranslateModule, TranslateLoader, TranslateFakeLoader } from '@ngx-translate/core';
import { ContentActionRef } from '@alfresco/adf-extensions';
import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions';
import { QueryList } from '@angular/core';
describe('ToolbarMenuComponent', () => {
let fixture: ComponentFixture<ToolbarMenuComponent>;
@ -50,6 +51,7 @@ describe('ToolbarMenuComponent', () => {
component = fixture.componentInstance;
component.matTrigger = jasmine.createSpyObj('MatMenuTrigger', ['closeMenu']);
component.actionRef = actions;
fixture.detectChanges();
});
@ -59,4 +61,23 @@ describe('ToolbarMenuComponent', () => {
fixture.detectChanges();
expect(component.matTrigger.closeMenu).toHaveBeenCalled();
});
it('should populate underlying menu with toolbar items', () => {
component.toolbarMenuItems = new QueryList();
component.toolbarMenuItems.reset([{ menuItem: {} } as any]);
expect(component.toolbarMenuItems.length).toBe(1);
expect(component.menu._allItems.length).toBe(0);
component.ngAfterViewInit();
expect(component.menu._allItems.length).toBe(1);
});
it('should track elements by content action id', () => {
const contentActionRef: ContentActionRef = {
id: 'action1',
type: ContentActionType.button
};
expect(component.trackByActionId(0, contentActionRef)).toBe('action1');
});
});

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);
}
canActivate(_: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean {
canActivate(_: ActivatedRouteSnapshot): Observable<boolean> {
return this.isQuickShareEnabled$;
}
canActivateChild(route: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean {
canActivateChild(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.canActivate(route);
}
}

View File

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

View File

@ -39,15 +39,20 @@ import {
ExtensionConfig,
NavBarGroupRef
} from '@alfresco/adf-extensions';
import { AppConfigService } from '@alfresco/adf-core';
import { AppConfigService, LogService } from '@alfresco/adf-core';
import { provideMockStore } from '@ngrx/store/testing';
import { hasQuickShareEnabled } from '@alfresco/aca-shared/rules';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
describe('AppExtensionService', () => {
let service: AppExtensionService;
let store: Store<AppStore>;
let extensions: ExtensionService;
let appConfigService: AppConfigService;
let logService: LogService;
let iconRegistry: MatIconRegistry;
let sanitizer: DomSanitizer;
beforeEach(() => {
TestBed.configureTestingModule({
@ -55,6 +60,8 @@ describe('AppExtensionService', () => {
providers: [provideMockStore({ initialState })]
});
iconRegistry = TestBed.inject(MatIconRegistry);
sanitizer = TestBed.inject(DomSanitizer);
appConfigService = TestBed.inject(AppConfigService);
store = TestBed.inject(Store);
@ -62,6 +69,7 @@ describe('AppExtensionService', () => {
service.repository.status.isQuickShareEnabled = true;
extensions = TestBed.inject(ExtensionService);
logService = TestBed.inject(LogService);
});
const applyConfig = (config: ExtensionConfig, selection?: boolean) => {
@ -78,6 +86,40 @@ describe('AppExtensionService', () => {
};
describe('configs', () => {
it('should log an error during setup', async () => {
spyOn(extensions, 'load').and.returnValue(Promise.resolve(null));
spyOn(logService, 'error').and.stub();
await service.load();
expect(service.config).toBeNull();
expect(logService.error).toHaveBeenCalledWith('Extension configuration not found');
});
it('should load content metadata presets', () => {
applyConfig({
$id: 'test',
$name: 'test',
$version: '1.0.0',
$license: 'MIT',
$vendor: 'Good company',
$runtime: '1.5.0',
features: {
'content-metadata-presets': [
{
id: 'app.content.metadata.kitten-images',
'kitten-images': {
id: 'app.content.metadata.kittenAspect',
'custom:aspect': '*',
'exif:exif': ['exif:pixelXDimension', 'exif:pixelYDimension']
}
}
]
}
});
expect(service.contentMetadata).toBeDefined();
});
it('should merge two arrays based on [id] keys', () => {
const left = [
{
@ -1074,6 +1116,107 @@ describe('AppExtensionService', () => {
});
});
describe('rules', () => {
it('should evaluate rule', () => {
extensions.setEvaluators({
rule1: () => true
});
expect(service.evaluateRule('rule1')).toBeTrue();
});
it('should not evaluate missing rule and return [false] by default', () => {
expect(service.evaluateRule('missing')).toBeFalse();
});
it('should confirm the rule is defined', () => {
extensions.setEvaluators({
rule1: () => true
});
expect(service.isRuleDefined('rule1')).toBeTrue();
});
it('should not confirm the rule is defined', () => {
expect(service.isRuleDefined(null)).toBeFalse();
expect(service.isRuleDefined('')).toBeFalse();
expect(service.isRuleDefined('missing')).toBeFalse();
});
it('should allow node preview', () => {
extensions.setEvaluators({
'app.canPreview': () => true
});
service.viewerRules.canPreview = 'app.canPreview';
expect(service.canPreviewNode(null)).toBeTrue();
});
it('should allow node preview with no rules', () => {
service.viewerRules = {};
expect(service.canPreviewNode(null)).toBeTrue();
});
it('should not allow node preview', () => {
extensions.setEvaluators({
'app.canPreview': () => false
});
service.viewerRules.canPreview = 'app.canPreview';
expect(service.canPreviewNode(null)).toBeFalse();
});
it('should allow viewer navigation', () => {
extensions.setEvaluators({
'app.allowNavigation': () => true
});
service.viewerRules.showNavigation = 'app.allowNavigation';
expect(service.canShowViewerNavigation(null)).toBeTrue();
});
it('should allow viewer navigation with no rules', () => {
service.viewerRules.showNavigation = null;
expect(service.canShowViewerNavigation(null)).toBeTrue();
});
it('should not allow viewer navigation', () => {
extensions.setEvaluators({
'app.allowNavigation': () => false
});
service.viewerRules.showNavigation = 'app.allowNavigation';
expect(service.canShowViewerNavigation(null)).toBeFalse();
});
it('should confirm the viewer extension is disabled explicitly', () => {
const extension = {
disabled: true
};
expect(service.isViewerExtensionDisabled(extension)).toBeTrue();
});
it('should confirm the viewer extension is disabled via rules', () => {
extensions.setEvaluators({
'viewer.disabled': () => true
});
const extension = {
disabled: false,
rules: {
disabled: 'viewer.disabled'
}
};
expect(service.isViewerExtensionDisabled(extension)).toBeTrue();
});
it('should confirm viewer extension is not disabled by default', () => {
expect(service.isViewerExtensionDisabled({})).toBeFalse();
});
});
describe('rule disable', () => {
beforeEach(() => {
extensions.setEvaluators({
@ -1425,4 +1568,107 @@ describe('AppExtensionService', () => {
});
});
});
describe('custom icons', () => {
it('should register custom icons', () => {
spyOn(iconRegistry, 'addSvgIconInNamespace').and.stub();
const rawUrl = './assets/images/ft_ic_ms_excel.svg';
applyConfig({
$id: 'test',
$name: 'test',
$version: '1.0.0',
$license: 'MIT',
$vendor: 'Good company',
$runtime: '1.5.0',
features: {
icons: [
{
id: 'adf:excel_thumbnail',
value: rawUrl
}
]
}
});
const url = sanitizer.bypassSecurityTrustResourceUrl(rawUrl);
expect(iconRegistry.addSvgIconInNamespace).toHaveBeenCalledWith('adf', 'excel_thumbnail', url);
});
it('should warn if icon has no url path', () => {
const warn = spyOn(logService, 'warn').and.stub();
applyConfig({
$id: 'test',
$name: 'test',
$version: '1.0.0',
$license: 'MIT',
$vendor: 'Good company',
$runtime: '1.5.0',
features: {
icons: [
{
id: 'adf:excel_thumbnail'
}
]
}
});
expect(warn).toHaveBeenCalledWith('Missing icon value for "adf:excel_thumbnail".');
});
it('should warn if icon has incorrect format', () => {
const warn = spyOn(logService, 'warn').and.stub();
applyConfig({
$id: 'test',
$name: 'test',
$version: '1.0.0',
$license: 'MIT',
$vendor: 'Good company',
$runtime: '1.5.0',
features: {
icons: [
{
id: 'incorrect.format',
value: './assets/images/ft_ic_ms_excel.svg'
}
]
}
});
expect(warn).toHaveBeenCalledWith(`Incorrect icon id format.`);
});
});
it('should resolve main action', (done) => {
extensions.setEvaluators({
'action.enabled': () => true
});
applyConfig({
$id: 'test',
$name: 'test',
$version: '1.0.0',
$license: 'MIT',
$vendor: 'Good company',
$runtime: '1.5.0',
features: {
mainAction: {
id: 'action-id',
title: 'action-title',
type: 'button',
rules: {
visible: 'action.enabled'
}
}
}
});
service.getMainAction().subscribe((action) => {
expect(action.id).toEqual('action-id');
done();
});
});
});

View File

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

View File

@ -30,60 +30,49 @@ import {
AppConfigService,
AlfrescoApiService,
PageTitleService,
UserPreferencesService,
AlfrescoApiServiceMock,
TranslationMock,
TranslationService
} from '@alfresco/adf-core';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { HttpClientModule } from '@angular/common/http';
import { SharedLinksApiService, GroupService, SearchQueryBuilderService, UploadService, DiscoveryApiService } from '@alfresco/adf-content-services';
import { ActivatedRoute, Router } from '@angular/router';
import { ContentApiService } from './content-api.service';
import { RouterExtensionService } from './router.extension.service';
import { OverlayContainer } from '@angular/cdk/overlay';
import { AppStore, STORE_INITIAL_APP_DATA } from '../../../store/src/states/app.state';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import {
DiscoveryApiService,
FileUploadErrorEvent,
GroupService,
SearchQueryBuilderService,
SharedLinksApiService,
UploadService
} from '@alfresco/adf-content-services';
import { ActivatedRoute } from '@angular/router';
import { STORE_INITIAL_APP_DATA } from '../../../store/src/states/app.state';
import { provideMockStore } from '@ngrx/store/testing';
import { CommonModule } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { TranslateServiceMock } from '../testing/translation.service';
import { RouterTestingModule } from '@angular/router/testing';
import { RepositoryInfo } from '@alfresco/js-api';
import { AcaMobileAppSwitcherService } from './aca-mobile-app-switcher.service';
import { MatDialogModule } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import { SnackbarErrorAction } from '../../../store/src/actions/snackbar.actions';
import { ContentApiService } from './content-api.service';
import { SetRepositoryInfoAction, SetUserProfileAction } from '../../../store/src/actions/app.actions';
describe('AppService', () => {
let service: AppService;
let auth: AuthenticationService;
let appConfig: AppConfigService;
let searchQueryBuilderService: SearchQueryBuilderService;
let userPreferencesService: UserPreferencesService;
let router: Router;
let activatedRoute: ActivatedRoute;
let routerExtensionService: RouterExtensionService;
let pageTitleService: PageTitleService;
let uploadService: UploadService;
let contentApiService: ContentApiService;
let store: Store;
let sharedLinksApiService: SharedLinksApiService;
let overlayContainer: OverlayContainer;
let alfrescoApiService: AlfrescoApiService;
let contentApi: ContentApiService;
let groupService: GroupService;
let storeInitialAppData: any;
let store: MockStore<AppStore>;
let acaMobileAppSwitcherService: AcaMobileAppSwitcherService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule, RouterTestingModule.withRoutes([]), MatDialogModule],
imports: [CommonModule, HttpClientModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), MatDialogModule],
providers: [
CommonModule,
SearchQueryBuilderService,
UserPreferencesService,
RouterExtensionService,
UploadService,
ContentApiService,
SharedLinksApiService,
OverlayContainer,
provideMockStore({}),
{
provide: PageTitleService,
@ -118,48 +107,19 @@ describe('AppService', () => {
isLoggedIn: () => false
}
},
{ provide: TranslationService, useClass: TranslationMock },
{ provide: TranslateService, useClass: TranslateServiceMock }
{ provide: TranslationService, useClass: TranslationMock }
]
});
appConfig = TestBed.inject(AppConfigService);
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);
acaMobileAppSwitcherService = TestBed.inject(AcaMobileAppSwitcherService);
service = new AppService(
userPreferencesService,
auth,
store,
router,
activatedRoute,
appConfig,
pageTitleService,
alfrescoApiService,
uploadService,
routerExtensionService,
contentApiService,
sharedLinksApiService,
groupService,
overlayContainer,
storeInitialAppData,
searchQueryBuilderService,
acaMobileAppSwitcherService
);
searchQueryBuilderService = TestBed.inject(SearchQueryBuilderService);
uploadService = TestBed.inject(UploadService);
store = TestBed.inject(Store);
sharedLinksApiService = TestBed.inject(SharedLinksApiService);
contentApi = TestBed.inject(ContentApiService);
groupService = TestBed.inject(GroupService);
service = TestBed.inject(AppService);
});
it('should be ready if [withCredentials] mode is used', (done) => {
@ -169,26 +129,7 @@ describe('AppService', () => {
}
};
const instance = new AppService(
userPreferencesService,
auth,
store,
router,
activatedRoute,
appConfig,
pageTitleService,
alfrescoApiService,
uploadService,
routerExtensionService,
contentApiService,
sharedLinksApiService,
groupService,
overlayContainer,
storeInitialAppData,
searchQueryBuilderService,
acaMobileAppSwitcherService
);
const instance = TestBed.inject(AppService);
expect(instance.withCredentials).toBeTruthy();
instance.ready$.subscribe(() => {
@ -204,4 +145,98 @@ describe('AppService', () => {
auth.onLogin.next();
await expect(isReady).toEqual(true);
});
it('should reset search to defaults upon logout', async () => {
const resetToDefaults = spyOn(searchQueryBuilderService, 'resetToDefaults');
auth.onLogout.next(true);
await expect(resetToDefaults).toHaveBeenCalled();
});
it('should rase notification on share link error', () => {
spyOn(store, 'select').and.returnValue(of(''));
service.init();
const dispatch = spyOn(store, 'dispatch');
sharedLinksApiService.error.next({ message: 'Error Message', statusCode: 1 });
expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('Error Message'));
});
it('should raise notification on upload error', async () => {
spyOn(store, 'select').and.returnValue(of(''));
service.init();
const dispatch = spyOn(store, 'dispatch');
uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 403 }));
expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.403'));
dispatch.calls.reset();
uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 404 }));
expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.404'));
dispatch.calls.reset();
uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 409 }));
expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.CONFLICT'));
dispatch.calls.reset();
uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 500 }));
expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.500'));
dispatch.calls.reset();
uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 504 }));
expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.504'));
dispatch.calls.reset();
uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 403 }));
expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.403'));
dispatch.calls.reset();
uploadService.fileUploadError.next(new FileUploadErrorEvent(null, {}));
expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.GENERIC'));
});
it('should load custom css', () => {
const appendChild = spyOn(document.head, 'appendChild');
spyOn(store, 'select').and.returnValue(of('/custom.css'));
service.init();
const cssLinkElement = document.createElement('link');
cssLinkElement.setAttribute('rel', 'stylesheet');
cssLinkElement.setAttribute('type', 'text/css');
cssLinkElement.setAttribute('href', '/custom.css');
expect(appendChild).toHaveBeenCalledWith(cssLinkElement);
});
it('should load repository status on login', () => {
const repository: any = {};
spyOn(contentApi, 'getRepositoryInformation').and.returnValue(of({ entry: { repository } }));
spyOn(store, 'select').and.returnValue(of(''));
service.init();
const dispatch = spyOn(store, 'dispatch');
auth.onLogin.next(true);
expect(dispatch).toHaveBeenCalledWith(new SetRepositoryInfoAction(repository));
});
it('should load user profile on login', async () => {
const person: any = { id: 'person' };
const group: any = { entry: {} };
const groups: any[] = [group];
spyOn(contentApi, 'getRepositoryInformation').and.returnValue(of({} as any));
spyOn(groupService, 'listAllGroupMembershipsForPerson').and.returnValue(Promise.resolve(groups));
spyOn(contentApi, 'getPerson').and.returnValue(of({ entry: person }));
spyOn(store, 'select').and.returnValue(of(''));
service.init();
const dispatch = spyOn(store, 'dispatch');
auth.onLogin.next(true);
await expect(groupService.listAllGroupMembershipsForPerson).toHaveBeenCalled();
await expect(dispatch).toHaveBeenCalledWith(new SetUserProfileAction({ person, groups: [group.entry] }));
});
});

View File

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

View File

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

View File

@ -26,16 +26,21 @@
import { Node } from '@alfresco/js-api';
export function isLocked(node: { entry: Node }): boolean {
const { entry } = node;
if (node?.entry) {
const { entry } = node;
return (
(entry && entry.isLocked) ||
(entry.properties && (entry.properties['cm:lockType'] === 'READ_ONLY_LOCK' || entry.properties['cm:lockType'] === 'WRITE_LOCK'))
);
return entry.isLocked || entry.properties?.['cm:lockType'] === 'READ_ONLY_LOCK' || entry.properties?.['cm:lockType'] === 'WRITE_LOCK';
} else {
return false;
}
}
export function isLibrary(node: { entry: Node | any }): boolean {
const { entry } = node;
if (node?.entry) {
const { entry } = node;
return (entry.guid && entry.id && entry.preset && entry.title && entry.visibility) || entry.nodeType === 'st:site';
return !!(entry.guid && entry.id && entry.preset && entry.title && entry.visibility) || entry.nodeType === 'st:site';
} else {
return false;
}
}

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/shared.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
// https://karma-runner.github.io/1.0/config/configuration-file.html
const { join } = require('path');
const getBaseKarmaConfig = require('../../karma.conf');
module.exports = function (config) {
const baseConfig = getBaseKarmaConfig();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
...baseConfig,
coverageReporter: {
...baseConfig.coverageReporter,
dir: join(__dirname, '../../coverage/aca-viewer'),
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/aca-viewer'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: true,
restartOnFileChange: true
});
};

View File

@ -1,5 +1,10 @@
{
"AOS": {
"ACTION_TITLE": "Edit in Microsoft Office™"
"ACTION_TITLE": "Edit in Microsoft Office™",
"ERRORS": {
"UNSUPPORTED_PLATFORM": "Only supported for Windows and MacOS platforms",
"ALREADY_LOCKED": "Document '{{ nodeId }}' is locked by '{{ lockOwner }}'",
"MISSING_PROTOCOL_HANDLER": "No protocol handler found for '{{ nodeName }}'"
}
}
}

View File

@ -1,35 +1,15 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const { join } = require('path');
const getBaseKarmaConfig = require('../../karma.conf');
module.exports = function(config) {
module.exports = function (config) {
const baseConfig = getBaseKarmaConfig();
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('karma-mocha-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
...baseConfig,
coverageReporter: {
...baseConfig.coverageReporter,
dir: join(__dirname, '../../coverage/aca-office-services-ext'),
},
coverageIstanbulReporter: {
dir: require('path').join(
__dirname,
'../../coverage/adf-office-services-ext'
),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['mocha', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: true
});
};

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 */
import { AppConfigService, AuthenticationService, NotificationService } from '@alfresco/adf-core';
import { AppConfigService, AuthenticationService, LogService, NotificationService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { MinimalNodeEntryEntity } from '@alfresco/js-api';
import { getFileExtension, supportedExtensions } from '@alfresco/aca-shared/rules';
@ -38,9 +38,10 @@ export interface IAosEditOnlineService {
})
export class AosEditOnlineService implements IAosEditOnlineService {
constructor(
private alfrescoAuthenticationService: AuthenticationService,
private authenticationService: AuthenticationService,
private appConfigService: AppConfigService,
private notificationService: NotificationService
private notificationService: NotificationService,
private logService: LogService
) {}
onActionEditOnlineAos(node: MinimalNodeEntryEntity): void {
@ -51,10 +52,10 @@ export class AosEditOnlineService implements IAosEditOnlineService {
// );
const checkedOut = node.properties['cm:lockType'] === 'WRITE_LOCK' || node.properties['cm:lockType'] === 'READ_ONLY_LOCK';
const lockOwner = node.properties['cm:lockOwner'];
const differentLockOwner = lockOwner.id !== this.alfrescoAuthenticationService.getEcmUsername();
const differentLockOwner = lockOwner.id !== this.authenticationService.getEcmUsername();
if (checkedOut && differentLockOwner) {
this.onAlreadyLockedNotification(node.id, lockOwner);
this.onAlreadyLockedNotification(node.id, lockOwner.id);
} else {
this.triggerEditOnlineAos(node);
}
@ -77,17 +78,21 @@ export class AosEditOnlineService implements IAosEditOnlineService {
}
private onAlreadyLockedNotification(nodeId: string, lockOwner: string) {
this.notificationService.openSnackMessage(`Document ${nodeId} locked by ${lockOwner}`, 3000);
this.logService.error('Document already locked by another user');
this.notificationService.showError(`AOS.ERRORS.ALREADY_LOCKED`, null, {
nodeId,
lockOwner
});
}
private getProtocolForFileExtension(fileExtension: string) {
return supportedExtensions[fileExtension];
return fileExtension && supportedExtensions[fileExtension];
}
private triggerEditOnlineAos(node: MinimalNodeEntryEntity): void {
const aosHost = this.appConfigService.get('aosHost');
let url: string;
const pathElements = (node.path.elements || []).map((segment) => segment.name);
const pathElements = (node.path?.elements || []).map((segment) => segment.name);
if (!pathElements.length) {
url = `${aosHost}/Company Home/_aos_nodeid/${this.getNodeId(node)}/${encodeURIComponent(node.name)}`;
@ -106,23 +111,25 @@ export class AosEditOnlineService implements IAosEditOnlineService {
const protocolHandler = this.getProtocolForFileExtension(fileExtension);
if (protocolHandler === undefined) {
this.notificationService.openSnackMessage(`No protocol handler found for {fileExtension}`, 3000);
this.logService.error('Protocol handler missing');
this.notificationService.showError(`AOS.ERRORS.MISSING_PROTOCOL_HANDLER`, null, { nodeName: node.name });
return;
}
if (!this.isWindows() && !this.isMacOs()) {
this.notificationService.openSnackMessage('Only supported for Windows and Mac', 3000);
this.logService.error('Unsupported platform');
this.notificationService.showError('AOS.ERRORS.UNSUPPORTED_PLATFORM');
} else {
this.aosTryToLaunchOfficeByMsProtocolHandler(protocolHandler, url);
this.openByUrl(protocolHandler, url);
}
}
private aosTryToLaunchOfficeByMsProtocolHandler(protocolHandler: string, url: string) {
const protocolUrl = protocolHandler + ':ofe%7Cu%7C' + url;
openByUrl(protocolHandler: string, url: string) {
const finalUrl = protocolHandler + ':ofe%7Cu%7C' + url;
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = protocolUrl;
iframe.src = finalUrl;
document.body.appendChild(iframe);