Skip to content

Commit

Permalink
Merge pull request #610 from mitre-attack/nav-unit-testing
Browse files Browse the repository at this point in the history
Navigator Unit Testing
  • Loading branch information
clemiller authored Feb 4, 2024
2 parents 0b68f34 + a1ef7bf commit 912fd23
Show file tree
Hide file tree
Showing 45 changed files with 4,578 additions and 222 deletions.
1 change: 1 addition & 0 deletions nav-app/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"options": {
"main": "src/test.ts",
"karmaConfig": "./karma.conf.js",
"codeCoverageExclude": ["src/polyfills.ts", "src/app/utils/taxii2lib.ts"],
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"scripts": [
Expand Down
4 changes: 2 additions & 2 deletions nav-app/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ module.exports = function (config) {
coverageReporter: {
type: 'html',
dir: 'coverage/',
subdir: 'chrome',
file: 'index.html'
subdir: 'chrome',
file: 'index.html',
},
});
};
90 changes: 82 additions & 8 deletions nav-app/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,96 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { deleteCookie, getCookie, hasCookie, setCookie } from './utils/cookies';
import { TabsComponent } from './tabs/tabs.component';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';

describe('AppComponent', () => {
let fixture: ComponentFixture<AppComponent>;
let app: any;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [AppComponent],
imports: [HttpClientTestingModule, MatDialogModule, MatSnackBarModule],
declarations: [AppComponent, TabsComponent],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
app = fixture.debugElement.componentInstance;
}));

it('should create the app', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'ATT&CK® Navigator'`, waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;

it('should intialize title', waitForAsync(() => {
app.ngOnInit();
let result = app.titleService.getTitle();
expect(result).toEqual(app.title);
}));

it(`should have title 'ATT&CK® Navigator'`, waitForAsync(() => {
expect(app.title).toEqual('ATT&CK® Navigator');
}));

it('should set user theme to theme-override-dark', waitForAsync(() => {
setCookie('is_user_theme_dark', 'true', 1);
// recreate component
fixture = TestBed.createComponent(AppComponent);
app = fixture.componentInstance;
expect(app.user_theme).toEqual('theme-override-dark');
}));

it('should set user theme to theme-override-light', waitForAsync(() => {
setCookie('is_user_theme_dark', 'false', 1);
// recreate component
fixture = TestBed.createComponent(AppComponent);
app = fixture.componentInstance;
expect(app.user_theme).toEqual('theme-override-light');
}));

it('should set user theme to theme-use-system', waitForAsync(() => {
deleteCookie('is_user_theme_dark');
// recreate component
fixture = TestBed.createComponent(AppComponent);
app = fixture.componentInstance;
expect(app.user_theme).toEqual('theme-use-system');
}));

it('should handle dark theme change', waitForAsync(() => {
app.themeChangeHandler('dark');
expect(app.user_theme).toEqual('theme-override-dark');
expect(hasCookie('is_user_theme_dark')).toBeTrue();
expect(getCookie('is_user_theme_dark')).toEqual('true');
}));

it('should handle light theme change', waitForAsync(() => {
app.themeChangeHandler('light');
expect(app.user_theme).toEqual('theme-override-light');
expect(hasCookie('is_user_theme_dark')).toBeTrue();
expect(getCookie('is_user_theme_dark')).toEqual('false');
}));

it('should handle system theme change', waitForAsync(() => {
setCookie('is_user_theme_dark', 'true', 1);

app.themeChangeHandler('system');
expect(app.user_theme).toEqual('theme-use-system');
expect(hasCookie('is_user_theme_dark')).toBeFalse();
}));

it('should prompt to navigate away', waitForAsync(() => {
app.configService.setFeature('leave_site_dialog', true);
let prompt = 'Are you sure you want to navigate away? Your data may be lost!';
let event$ = { returnValue: null };
app.promptNavAway(event$);
expect(event$.returnValue).toEqual(prompt);
}));

it('should not prompt to navigate away', waitForAsync(() => {
app.configService.setFeature('leave_site_dialog', false);
let event$ = { returnValue: null };
app.promptNavAway(event$);
expect(event$.returnValue).toEqual(null);
}));
});
2 changes: 1 addition & 1 deletion nav-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class AppComponent implements OnInit {
constructor(
public configService: ConfigService,
private iconsService: IconsService,
private titleService: Title
public titleService: Title
) {
Array.prototype.includes = function (value): boolean {
for (let i = 0; i < this.length; i++) {
Expand Down
2 changes: 1 addition & 1 deletion nav-app/src/app/classes/stix/data-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class DataComponent extends StixObject {
if (relationships.has(this.id)) {
relationships.get(this.id).forEach((targetID) => {
const technique = domain.techniques.find((t) => t.id === targetID);
if (technique) techniques.push(technique);
if (technique) techniques.push(technique.id);
});
}
return techniques;
Expand Down
4 changes: 2 additions & 2 deletions nav-app/src/app/classes/view-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class ViewModel {
public loaded: boolean = false; // whether or not techniqueVMs are loaded

public techniqueVMs: Map<string, TechniqueVM> = new Map<string, TechniqueVM>(); // configuration for each technique
private selectedTechniques: Set<string> = new Set<string>(); // currently selected techniques (technique_tactic_id)
public selectedTechniques: Set<string> = new Set<string>(); // currently selected techniques (technique_tactic_id)
public activeTvm: TechniqueVM; // first selected techniqueVM

private linkMismatches: string[] = []; // subsequent selected technique_tactic_ids that do not have matching links
Expand Down Expand Up @@ -120,7 +120,7 @@ export class ViewModel {
name: string,
uid: string,
domainVersionID: string,
private dataService: DataService
public dataService: DataService
) {
console.debug("initializing ViewModel '" + name + "'");
this.domainVersionID = domainVersionID;
Expand Down
92 changes: 90 additions & 2 deletions nav-app/src/app/help/help.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog';
import { HelpComponent } from './help.component';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { MarkdownService, MarkdownModule } from 'ngx-markdown';
import { Renderer2 } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

describe('HelpComponent', () => {
let component: HelpComponent;
let fixture: ComponentFixture<HelpComponent>;
let markdownService: MarkdownService;
let dialog: MatDialog;
let renderer: Renderer2;
let mockMarkdownElement: any;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule, MatDialogModule, MarkdownModule.forRoot({ loader: HttpClient })],
imports: [HttpClientModule, MatDialogModule, MarkdownModule.forRoot({ loader: HttpClient }), BrowserAnimationsModule],
declarations: [HelpComponent],
providers: [
{
Expand All @@ -21,17 +28,98 @@ describe('HelpComponent', () => {
useValue: {},
},
MarkdownService,
{
provide: Renderer2,
useValue: {
listen: jasmine.createSpy('listen').and.returnValue(() => {}),
},
},
],
}).compileComponents();
dialog = TestBed.inject(MatDialog);
markdownService = TestBed.inject(MarkdownService);
renderer = TestBed.inject(Renderer2);
}));

beforeEach(() => {
fixture = TestBed.createComponent(HelpComponent);
component = fixture.componentInstance;
renderer = fixture.componentRef.injector.get<Renderer2>(Renderer2);
mockMarkdownElement = {
element: {
nativeElement: document.createElement('div'),
},
};
component['markdownElement'] = mockMarkdownElement;
spyOn(renderer, 'listen').and.callFake((elem, eventName, callback) => {
return () => {};
});
fixture.detectChanges();
spyOn(component, 'scrollTo').and.callThrough();
spyOn(dialog, 'open').and.callThrough();
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

describe('ngOnDestroy', () => {
it('should remove event listeners if any', () => {
component['listenObj'] = jasmine.createSpy();
component.ngOnDestroy();
expect(component['listenObj']).toHaveBeenCalled();
});
});

describe('scrollTo', () => {
it('should call scrollIntoView when element exists', () => {
const mockElement = document.createElement('div');
spyOn(document, 'querySelector').and.returnValue(mockElement);
spyOn(mockElement, 'scrollIntoView');
component.scrollTo('toc');
expect(document.querySelector).toHaveBeenCalledWith('.toc');
expect(mockElement.scrollIntoView).toHaveBeenCalledWith({
behavior: 'smooth',
block: 'start',
inline: 'nearest',
});
});

it('should not call scrollIntoView when element does not exist', () => {
spyOn(document, 'querySelector').and.returnValue(null);
const scrollSpy = spyOn(window, 'scrollTo');
component.scrollTo('toc');
expect(document.querySelector).toHaveBeenCalledWith('.toc');
expect(scrollSpy).not.toHaveBeenCalled();
});
});

describe('openLayerDialog', () => {
it('should open the dialog with LayerInformationComponent', () => {
component.openLayerDialog();
expect(dialog.open).toHaveBeenCalled();
});
});

describe('onMarkdownLoad', () => {
it('should set up click listener on markdown element', () => {
component.onMarkdownLoad(null);
expect(renderer.listen).toHaveBeenCalled();
});
});

describe('MarkdownService renderer overrides', () => {
it('should override heading renderer', () => {
const mockHeading = 'Heading';
const level = 1;
markdownService.renderer.heading(mockHeading, level, null, null);
expect(component.headingAnchors.length).toBeGreaterThan(0);
});

it('should override html renderer', () => {
const mockHtml = '<div></div>';
markdownService.renderer.html(mockHtml);
});
});
});
Loading

0 comments on commit 912fd23

Please sign in to comment.