diff --git a/frontend/src/components/export/InstanceExport.vue b/frontend/src/components/export/InstanceExport.vue index 681e0e6b9958f92c2c2db814a320b2362d90ed1a..844a0ab4d6845c90838cbb7648b9dc895701ef10 100644 --- a/frontend/src/components/export/InstanceExport.vue +++ b/frontend/src/components/export/InstanceExport.vue @@ -42,7 +42,7 @@ import { exportMixin, ExportType } from '@/components/mixins/mixins' export default class DataExport extends exportMixin { exportDialog = true mapFile: File | null = null - exportType = ExportType.JSON // instance export is only available as JSON + exportType = ExportType.JSON // instance export is only available as JSON get studentMap () { return getters.state.studentMap } diff --git a/frontend/src/components/mixins/mixins.ts b/frontend/src/components/mixins/mixins.ts index 972371d08fd6f147cd4264f6836e1b75413c655a..bee6fc96f26736009064cc15d2944432acaedf2d 100644 --- a/frontend/src/components/mixins/mixins.ts +++ b/frontend/src/components/mixins/mixins.ts @@ -45,7 +45,7 @@ export class exportMixin extends Vue { } else { throw new Error('Unsupported export type') } - + if (this.mapFile || this.mapFileLoaded) { this.getMappedExportFile(studentData) } else { @@ -85,7 +85,7 @@ export class exportMixin extends Vue { }, '') + '\n' // add trailing newline } - optionalConvertAndCreatePopup(studentData: StudentExportItem[] | InstanceExportData) { + optionalConvertAndCreatePopup (studentData: StudentExportItem[] | InstanceExportData) { const convertedData = this.exportType === ExportType.CSV ? this.jsonToCSV(studentData as StudentExportItem[]) : studentData // we have a cast here because only student export may be converted to csv @@ -136,6 +136,6 @@ export class exportMixin extends Vue { this.exportDialog = true } - applyMapping (exportData: StudentExportItem[] | InstanceExportData): void { throw new Error("Not implemented.") } - createDownloadPopup (content: string | StudentExportItem[] | InstanceExportData, fileType: ExportType): void { throw new Error("Not implemented.") } + applyMapping (exportData: StudentExportItem[] | InstanceExportData): void { throw new Error('Not implemented.') } + createDownloadPopup (content: string | StudentExportItem[] | InstanceExportData, fileType: ExportType): void { throw new Error('Not implemented.') } } diff --git a/frontend/tests/unit/components/AutoLogout.spec.ts b/frontend/tests/unit/components/AutoLogout.spec.ts index 5a5730f68f8b1ca55742d89365a85c028793c863..57501f45fdc2419a49dd0fa8f1e39cceae74fdf0 100644 --- a/frontend/tests/unit/components/AutoLogout.spec.ts +++ b/frontend/tests/unit/components/AutoLogout.spec.ts @@ -15,15 +15,15 @@ describe('Auto Logout Unit Tests', () => { let store: any = null let consoleTemp = { warn: console.warn, - error: console.error, + error: console.error } - before(function() { - console.warn = function() {} - console.error = function() {} + before(function () { + console.warn = function () {} + console.error = function () {} }) - after(function() { + after(function () { console.warn = consoleTemp.warn console.error = consoleTemp.error }) @@ -37,7 +37,7 @@ describe('Auto Logout Unit Tests', () => { afterEach(() => { sinon.restore() }) - + it('should be hidden by default', () => { let wrapper = mount(AutoLogout, { localVue: localVue, store }) wrapper.vm.$data.logoutDialog.should.equal(false) diff --git a/frontend/tests/unit/mixins/exportMixin.spec.ts b/frontend/tests/unit/mixins/exportMixin.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6c6cdd0570469bbbaf6cac9f04a927bc57eb4d1 --- /dev/null +++ b/frontend/tests/unit/mixins/exportMixin.spec.ts @@ -0,0 +1,153 @@ +import { exportMixin, ExportType } from '@/components/mixins/mixins' +import sinon from 'sinon' +import ax from '@/api' +import testUtils, { FakeFileReader } from '@/../tests/utils/testUtils' +import chai from 'chai' +import { mutations as mut } from '@/store/mutations' +chai.should() + +describe('Export Mixin Unit Tests', () => { + describe('getExportFile()', () => { + let e = new exportMixin() + + afterEach(() => { + sinon.restore() + }) + + it('should fetch from the dataExport endpoint when called with "data"', async () => { + let stub = sinon.stub().resolves({ data: testUtils.studentExports }) + sinon.replace(ax, 'post', stub) + sinon.replace(e, 'optionalConvertAndCreatePopup', () => {}) + await e.getExportFile('data') + stub.calledWith('/api/export/json/').should.equal(true) + }) + it('should fetch from instanceExport endpoint when called with "instance"', async () => { + let stub = sinon.stub().resolves({ data: testUtils.instanceExports }) + sinon.replace(ax, 'get', stub) + sinon.replace(e, 'optionalConvertAndCreatePopup', () => {}) + await e.getExportFile('instance') + stub.calledWith('/api/instance/export').should.equal(true) + }) + it('should try to apply mapping when map file is given', async () => { + let spy = sinon.spy() + e.mapFile = testUtils.fakeFile + sinon.replace(ax, 'post', sinon.stub().resolves({ data: testUtils.studentExports })) + sinon.replace(e, 'getMappedExportFile', spy) + await e.getExportFile('data') + e.mapFile = null + spy.calledOnce.should.equal(true) + spy.calledWith(testUtils.studentExports).should.equal(true) + }) + it('should call popup creation method with proper arguments when no mapping file is given', async () => { + let spy = sinon.spy() + sinon.replace(ax, 'post', sinon.stub().resolves({ data: testUtils.studentExports })) + sinon.replace(e, 'optionalConvertAndCreatePopup', spy) + await e.getExportFile('data') + spy.calledOnce.should.equal(true) + spy.calledWith(testUtils.studentExports).should.equal(true) + }) + }) + describe('optionalConvertAndCreatePopup()', () => { + let e = new exportMixin() + + afterEach(() => { + sinon.restore() + }) + + it('should convert data to csv when csv is selected in dropdown', () => { + let stub = sinon.stub().returns('students as csv') + let spy = sinon.spy() + e.exportType = ExportType.CSV + sinon.replace(e, 'jsonToCSV', stub) + sinon.replace(e, 'createDownloadPopup', spy) + e.optionalConvertAndCreatePopup(testUtils.studentExports) + stub.calledWith(testUtils.studentExports).should.equal(true) + spy.calledWith('students as csv', ExportType.CSV).should.equal(true) + }) + }) + describe('readMapFileAndCommit', () => { + let e = new exportMixin() + // @ts-ignore + global.FileReader = FakeFileReader + + afterEach(() => { + sinon.restore() + }) + + it('should throw an error when no mapfile was given', () => { + e.readMapFileAndCommit().catch((res) => { + res.should.not.equal(undefined) + }) + }) + it('should read selected mapFile', async () => { + e.mapFile = testUtils.fakeFile + let stub = sinon.stub() + // @ts-ignore + sinon.replace(global.FileReader.prototype, 'readAsText', stub) + e.readMapFileAndCommit() + e.mapFile = null + stub.args[0][0].name.should.equal(testUtils.fakeFile.name) + }) + it('should parse file and trigger mutation when file was read', async () => { + e.mapFile = testUtils.fakeFile + let stub = sinon.stub().returns(testUtils.studentMap) + let mutSpy = sinon.spy() + sinon.replace(e, 'parseCSVMap', stub) + sinon.replace(mut, 'SET_STUDENT_MAP', mutSpy) + await e.readMapFileAndCommit() + e.mapFile = null + stub.calledOnce.should.equal(true) + stub.calledWithExactly('Grady testUtils fake result').should.equal(true) + mutSpy.calledWithExactly(testUtils.studentMap).should.equal(true) + }) + }) + describe('getMappedExportFile', () => { + let e = new exportMixin + + afterEach(() => { + sinon.restore() + }) + + it('should read mapFile if it is not already loaded when used with instanceExports then apply mapping and create popup', async () => { + e.mapFile = testUtils.fakeFile + let spy = sinon.spy() + sinon.replace(e, 'readMapFileAndCommit', spy) + sinon.replace(e, 'applyMapping', () => {}) + sinon.replace(e, 'optionalConvertAndCreatePopup', () => {}) + await e.getMappedExportFile(testUtils.instanceExports) + e.mapFile = null + spy.called.should.equal(true) + }) + it('should read mapFile if it is not already loaded when used with dataExport then apply mapping and create popup', async () => { + e.mapFile = testUtils.fakeFile + let spy = sinon.spy() + sinon.replace(e, 'readMapFileAndCommit', spy) + sinon.replace(e, 'applyMapping', () => {}) + sinon.replace(e, 'optionalConvertAndCreatePopup', () => {}) + await e.getMappedExportFile(testUtils.studentExports) + e.mapFile = null + spy.called.should.equal(true) + }) + it('should apply the mapping and then open popup when mapFile is loaded', async () => { + sinon.replaceGetter(e, 'mapFileLoaded', () => { return true }) + let mappingSpy = sinon.spy() + let popupSpy = sinon.spy() + sinon.replace(e, 'applyMapping', mappingSpy) + sinon.replace(e, 'optionalConvertAndCreatePopup', popupSpy) + await e.getMappedExportFile(testUtils.instanceExports) + e.mapFile = null + mappingSpy.calledWithExactly(testUtils.instanceExports).should.equal(true) + popupSpy.calledWithExactly(testUtils.instanceExports).should.equal(true) + }) + }) + describe('jsonToCSV()', () => { + let e = new exportMixin + + it('should correctly parse JSON input to CSV with ; delimiter', () => { + let csv = e.jsonToCSV(testUtils.studentExports) + let expected = 'Matrikel;Name;Username;Sum;Exam;Password;test01\n' + + '1000000;name;username;100;exam;pwd;100\n' + csv.should.equal(expected) + }) + }) +}) diff --git a/frontend/tests/utils/testUtils.ts b/frontend/tests/utils/testUtils.ts new file mode 100644 index 0000000000000000000000000000000000000000..599cb750c818286d2d087ec554b6456d16dcec9e --- /dev/null +++ b/frontend/tests/utils/testUtils.ts @@ -0,0 +1,40 @@ +import { StudentExportItem, InstanceExportData } from '@/api' + +export class FakeFileReader { + result: String = 'Grady testUtils fake result' + + constructor () {} + // readAsText will execute the callback that was provided to onload + readAsText (file: File) { this.onload({ target: this }) } + onload: Function = () => { } +} + +export default { + studentExports: <StudentExportItem[]> [{ + Matrikel: '1000000', + Name: 'name', + Username: 'username', + Sum: 100, + Exam: 'exam', + Password: 'pwd', + Scores: [{ type: 'test01', score: 100 }] + }], + instanceExports: <InstanceExportData> { + students: [{ + name: 'test', + matrikelNo: '1000000' + }] + }, + fakeFile: <File> { + lastModified: 1, + name: 'Grady testUtils fake file', + size: 1, + slice: (start?: number | undefined, end?: number | undefined, contentType?: string | undefined): Blob => { return new Blob() }, + type: 'fake file' + }, + studentMap: { + '1000000': { + matrikelNo: '1000000', name: 'test' + } + } +}