diff --git a/bower.json b/bower.json index 59f899ea..23fc7dd8 100644 --- a/bower.json +++ b/bower.json @@ -14,7 +14,9 @@ "angular-resource": "~1.2.0", "modernizr": "~2.6.2", "jquery-ui": "~1.10.3", - "momentjs": "~2.3.0" + "momentjs": "~2.3.0", + "jasmine-matchers": "https://github.com/JamieMason/Jasmine-Matchers.git", + "objectdiff": "https://github.com/NV/objectDiff.js.git" }, "dependencies": {} } diff --git a/karma/karma-unit.tpl.js b/karma/karma-unit.tpl.js index e68d69d7..6caeff88 100644 --- a/karma/karma-unit.tpl.js +++ b/karma/karma-unit.tpl.js @@ -16,10 +16,12 @@ module.exports = function (config) { /** * This is the list of file patterns to load into the browser during testing. */ - files: JSON.parse(fileJson).concat([ - 'src/**/*.js', - 'src/**/*.coffee' - ]), + files: [ + "vendor/objectdiff/objectDiff.js", + "vendor/jasmine-matchers/dist/jasmine-matchers.js" + ].concat(JSON.parse(fileJson).concat([ + 'src/**/*.js' + ])), exclude: [ 'src/assets/**/*.js' ], @@ -33,9 +35,9 @@ module.exports = function (config) { }, /** - * How to report, by default. + * How to report, by default. 'dots', 'progress' */ - reporters: 'dots', + reporters: ['dots'], /** * On which port should the browser connect, on which port is the test runner diff --git a/package.json b/package.json index 894a171b..92ccb6ff 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,36 @@ { - "author": "Anthony Truskinger", - "name": "baw-client", - "version": "0.0.4", - "description": "The AngularJS client for the QUT Bioacoustics server", - "licenses": { - "type": "Apache", - "url": "https://github.com/QutBioacoustics/baw-client/blob/master/LICENSE" - }, - "bugs": "https://github.com/QutBioacoustics/baw-client/issues", - "repository": { - "type": "git", - "url": "https://github.com/QutBioacoustics/baw-client.git" - }, - "//": "THE karma-chrome-launcher IS NEEDED TO LAUNCH CHROME PROPERLY ON WINDOWS. REMOVE WHEN PULL REQUEST COMPLETED https://github.com/karma-runner/karma-chrome-launcher", - "devDependencies": { - "grunt": "~0.4.1", - "grunt-contrib-clean": "~0.4.1", - "grunt-contrib-copy": "~0.4.1", - "grunt-contrib-jshint": "~0.4.3", - "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-watch": "~0.4.0", - "grunt-contrib-uglify": "~0.2.0", - "grunt-karma": "~0.7.0", - "karma-chrome-launcher": "https://github.com/EE/karma-chrome-launcher/tarball/windows", - "grunt-ngmin": "0.0.2", - "grunt-html2js": "~0.1.3", - "grunt-conventional-changelog": "~0.1.1", - "grunt-bump": "0.0.6", - "grunt-contrib-connect": "~0.5.0", - "connect-modrewrite": "~0.5.7", - "grunt-sass": "~0.7.0", - "lodash": "~2.2.1" - }, - "private": true + "author": "Anthony Truskinger", + "name": "baw-client", + "version": "0.0.4", + "description": "The AngularJS client for the QUT Bioacoustics server", + "licenses": { + "type": "Apache", + "url": "https://github.com/QutBioacoustics/baw-client/blob/master/LICENSE" + }, + "bugs": "https://github.com/QutBioacoustics/baw-client/issues", + "repository": { + "type": "git", + "url": "https://github.com/QutBioacoustics/baw-client.git" + }, + "//": "THE karma-chrome-launcher IS NEEDED TO LAUNCH CHROME PROPERLY ON WINDOWS. REMOVE WHEN PULL REQUEST COMPLETED https://github.com/karma-runner/karma-chrome-launcher", + "devDependencies": { + "grunt": "~0.4.1", + "grunt-contrib-clean": "~0.4.1", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-jshint": "~0.4.3", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-watch": "~0.4.0", + "grunt-contrib-uglify": "~0.2.0", + "grunt-karma": "~0.7.0", + "karma-chrome-launcher": "https://github.com/EE/karma-chrome-launcher/tarball/windows", + "grunt-ngmin": "0.0.2", + "grunt-html2js": "~0.1.3", + "grunt-conventional-changelog": "~0.1.1", + "grunt-bump": "0.0.6", + "grunt-contrib-connect": "~0.5.0", + "connect-modrewrite": "~0.5.7", + "grunt-sass": "~0.7.0", + "lodash": "~2.2.1" + }, + "private": true } diff --git a/src/app/listen/_listen.scss b/src/app/listen/_listen.scss index 00137882..f68d21e9 100644 --- a/src/app/listen/_listen.scss +++ b/src/app/listen/_listen.scss @@ -81,4 +81,16 @@ + +} + + +.details-table { + @extend .table; + @extend .table-striped; + @extend .table-bordered; + + tbody { + + } } \ No newline at end of file diff --git a/src/app/listen/listen.tpl.html b/src/app/listen/listen.tpl.html index a09cfd49..6f92649e 100644 --- a/src/app/listen/listen.tpl.html +++ b/src/app/listen/listen.tpl.html @@ -133,10 +133,11 @@
Selected | +Unsaved | Jump to | Annotation ID | Audio Recording | @@ -156,7 +157,10 @@- SELECTED:{{ae.selected}} + {{ae.selected && '✓' || '✗'}} + | ++ {{ae.isDirty && '✓' || '✗'}} | diff --git a/src/components/directives/bawAnnotationViewer.js b/src/components/directives/bawAnnotationViewer.js index 83354ea3..ce129cf9 100644 --- a/src/components/directives/bawAnnotationViewer.js +++ b/src/components/directives/bawAnnotationViewer.js @@ -1,6 +1,6 @@ var bawds = bawds || angular.module('bawApp.directives', ['bawApp.configuration']); -bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { +bawds.directive('bawAnnotationViewer', [ 'conf.paths', 'AudioEvent', function (paths, AudioEvent) { function variance(x, y) { var fraction = x / y; @@ -38,34 +38,37 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { throw "AnnotationEditor:calculateUnitConversions: can't determine natural height or natural width of source image!"; } - // crop images that are too tall - specifically for removing DC values - if (!baw.isPowerOfTwo(imageHeight)) { - var croppedHeight = baw.closestPowerOfTwoBelow(imageHeight); - console.error("AnnotationEditor:calculateUnitConversions: The natural height (" + imageHeight + - "px) for image " + image.src + - " is not a power of two. The image has been STRETCHED to " + croppedHeight + "px! ALL MEASUREMENTS ON THIS SPECTROGRAM WILL BE WRONG!"); - - // squish image into the nearest 'correct height' to minimise damage - result.enforcedImageHeight = imageHeight = croppedHeight; - } + // only process if image is loaded + if (imageHeight && imageWidth) { + // crop images that are too tall - specifically for removing DC values + if (!baw.isPowerOfTwo(imageHeight)) { + var croppedHeight = baw.closestPowerOfTwoBelow(imageHeight); + console.error("AnnotationEditor:calculateUnitConversions: The natural height (" + imageHeight + + "px) for image " + image.src + + " is not a power of two. The image has been STRETCHED to " + croppedHeight + "px! ALL MEASUREMENTS ON THIS SPECTROGRAM WILL BE WRONG!"); + + // squish image into the nearest 'correct height' to minimise damage + result.enforcedImageHeight = imageHeight = croppedHeight; + } - // use the image width to estimate the actual shown duration - var spectrogramBasedAudioLength = (imageWidth * window) / sampleRate, - spectrogramPps = imageWidth / spectrogramBasedAudioLength; + // use the image width to estimate the actual shown duration + var spectrogramBasedAudioLength = (imageWidth * window) / sampleRate, + spectrogramPps = imageWidth / spectrogramBasedAudioLength; - // use the image height to estimate the actual shown frequency bounds - var spectrogramPph = imageHeight / nyquistFrequency; + // use the image height to estimate the actual shown frequency bounds + var spectrogramPph = imageHeight / nyquistFrequency; - // do consistency check (tolerance = 2%) - var tolerance = 0.02; - if (variance(idealPph, spectrogramPph) > tolerance) { - console.warn("AnnotationEditor:calculateUniConversions: the image height does not conform well with the meta data. The image will be stretched to fit!", - idealPph, spectrogramPph); - } - if (variance(idealPps, spectrogramPps) > tolerance) { - console.warn("AnnotationEditor:calculateUniConversions: the image width does not conform well with the meta data. The image will be stretched to fit!", - idealPps, spectrogramPps); + // do consistency check (tolerance = 2%) + var tolerance = 0.02; + if (variance(idealPph, spectrogramPph) > tolerance) { + console.warn("AnnotationEditor:calculateUniConversions: the image height does not conform well with the meta data. The image will be stretched to fit!", + idealPph, spectrogramPph); + } + if (variance(idealPps, spectrogramPps) > tolerance) { + console.warn("AnnotationEditor:calculateUniConversions: the image width does not conform well with the meta data. The image will be stretched to fit!", + idealPps, spectrogramPps); + } } } @@ -104,7 +107,7 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { return Math.abs(conversions.nyquistFrequency - hertz); }, invertPixels: function invertPixels(pixels) { - return Math.abs(conversions.imageHeight - pixels); + return Math.abs(conversions.enforcedImageHeight - pixels); } }; } @@ -145,7 +148,7 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { function watchForSpectrogramChanges(scope, imageElement) { function updateUnitConverters() { - console.debug("AnnotationEditor:watchForSpectrogramChanges:"); + console.debug("AnnotationEditor:watchForSpectrogramChanges:updateUnitConverters"); scope.model.converters = calculateUnitConverters(scope.model.media, scope.$image[0]); @@ -162,7 +165,7 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { updateUnitConverters(); }); }, false); - updateUnitConverters(); + //updateUnitConverters(); } /**ȻɌɄƉ**/ @@ -179,13 +182,13 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { * Handles emitted create, update, delete */ function drawaboxUpdatesModel(scope, annotation, box, action) { - console.debug("AnnotationEditor:drawaboxUpdatesModel:"); + console.debug("AnnotationEditor:drawaboxUpdatesModel:", action); // invariants if (action === DRAWABOX_ACTION_SELECT && box.selected !== true) { throw "AnnotationEditor:drawaboxUpdatesModel: Invariant failed for selection action"; } - if (action !== DRAWABOX_ACTION_SELECT && box.selected !== annotation.selected) { + if (action !== DRAWABOX_ACTION_SELECT && action !== DRAWABOX_ACTION_CREATE && box.selected !== annotation.selected) { throw "AnnotationEditor:drawaboxUpdatesModel: Invariant failed for non-selection action"; } if (action !== DRAWABOX_ACTION_CREATE && !annotation) { @@ -193,9 +196,9 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { } // pre assertion - var wasDirty = annotation.isDirty; + var wasDirty = annotation === undefined ? null : annotation.isDirty; var boxId = baw.parseInt(box.id); - if (annotation.__localId__ !== boxId) { + if (annotation && annotation.__localId__ !== boxId) { console.error("Box ids do not match on resizing or move event", annotation.__localId__, boxId); return; } @@ -204,11 +207,11 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { // create if (action === DRAWABOX_ACTION_CREATE) { //noinspection AssignmentToFunctionParameterJS - annotation = new baw.Annotation(baw.parseInt(box.id), audioRecordingId); + annotation = new baw.Annotation(baw.parseInt(box.id), scope.model.media.id); scope.model.audioEvents.push(annotation); } - annotation.lastUpdater = UPDATER_DRAWABOX; + annotation.$lastUpdater = UPDATER_DRAWABOX; // only the select action selects, and only the select action does not update the bounds of the annotation if (action === DRAWABOX_ACTION_SELECT) { @@ -235,11 +238,17 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { }); // post assertion - if (action === DRAWABOX_ACTION_SELECT && annotation.isDirty) { - throw "AnnotationEditor:drawaboxUpdatesModel: Post condition failed for selection triggering a isDirty state"; + if (action === DRAWABOX_ACTION_SELECT) { + console.assert(annotation.isDirty == wasDirty, + "AnnotationEditor:drawaboxUpdatesModel: Post condition failed for selection triggering a isDirty state"); + } + else { + console.assert(annotation.isDirty, + "AnnotationEditor:drawaboxUpdatesModel: Post condition failed for action not triggering a isDirty state"); } - if (action === DRAWABOX_ACTION_DELETE && annotation.toBeDeleted !== true) { - throw "AnnotationEditor:drawaboxUpdatesModel: Post condition failed for ensuring a annotation is deleted"; + if (action === DRAWABOX_ACTION_DELETE) { + console.assert(annotation.toBeDeleted === true, + "AnnotationEditor:drawaboxUpdatesModel: Post condition failed for ensuring a annotation is deleted"); } } @@ -248,7 +257,7 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { * Handles model updates (not created or deleted events) */ function modelUpdatesDrawabox(scope, annotation) { - console.debug("AnnotationEditor:modelUpdatesDrawabox:"); + console.debug("AnnotationEditor:modelUpdatesDrawabox:", annotation.__localId__); var drawaboxInstance = scope.$drawaboxElement, top = scope.model.converters.invertPixels(scope.model.converters.hertzToPixels(annotation.highFrequencyHertz)), @@ -260,15 +269,32 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { } function modelUpdatesServer(annotation) { + // invariants - if (!annotation) { - throw "AnnotationEditor:modelUpdatesServer: Invalid state! Cannot call this method with a falsy value!"; + console.assert(annotation, + "AnnotationEditor:modelUpdatesServer: Invalid state! Cannot call this method with a falsy value!"); + console.assert(annotation.isDirty === true, + "AnnotationEditor:modelUpdatesServer: Invalid state! The annotation should be dirty (but isn't)!"); + + var postData = annotation.exportObj(); + var parameters = {recordingId: postData.audioRecordingId, audioEventId: postData.id}; + if (annotation.isNew()) { + console.debug("AnnotationEditor:modelUpdatesServer: create!", parameters.__localId__); } - if (annotation.isDirty !== true) { - throw "AnnotationEditor:modelUpdatesServer: Invalid state! The annotation should be dirty (but isn't)!"; + else if (annotation.toBeDeleted === true) { + console.debug("AnnotationEditor:modelUpdatesServer: delete!", parameters.__localId__); + } + else { + // update! + console.debug("AnnotationEditor:modelUpdatesServer: update!", parameters.__localId__); + AudioEvent.update(parameters, postData, + function success(value, headers) { + console.debug("AnnotationEditor:modelUpdatesServer: update success", value); + }, + function error(response) { + console.debug("AnnotationEditor:modelUpdatesServer: update FAILURE"); + }); } - - console.debug("AnnotationEditor:modelUpdatesServer: stub"); } function serverUpdatesModel() { @@ -296,20 +322,15 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { console.debug("AnnotationEditor:modelUpdated:", changedAnnotation.__localId__, changedAnnotation.selected); // invariants - if (changedAnnotation.lastUpdater === UPDATER_DRAWABOX && changedAnnotation.isDirty !== true) { - throw "AnnotationEditor:modelUpdated: Invalid state! If the last update came from drawabox then the the annotation must be dirty!"; - } - if (changedAnnotation.lastUpdater === UPDATER_PAGE_LOAD && changedAnnotation.isDirty !== false) { - throw "AnnotationEditor:modelUpdated: Invalid state! If the last update came from page load then the the annotation must NOT be dirty!"; - } - if (changedAnnotation.toBeDeleted && changedAnnotation.isDirty !== true) { - throw "AnnotationEditor:modelUpdated: Invalid state! If the the delete flag is set the annotation must be dirty!"; - } + console.assert(changedAnnotation.$lastUpdater !== UPDATER_PAGE_LOAD || changedAnnotation.isDirty !== false, + "AnnotationEditor:modelUpdated: Invalid state! If the last update came from page load then the the annotation must NOT be dirty!"); + console.assert(!changedAnnotation.toBeDeleted || changedAnnotation.toBeDeleted && changedAnnotation.isDirty !== true, + "AnnotationEditor:modelUpdated: Invalid state! If the the delete flag is set the annotation must be dirty!"); // if the last update was done by the drawabox control, do not propagate it back to drawabox - if (changedAnnotation.lastUpdater === UPDATER_DRAWABOX) { + if (changedAnnotation.$lastUpdater === UPDATER_DRAWABOX) { // reset flag - changedAnnotation.lastUpdater = null; + changedAnnotation.$lastUpdater = null; } else { modelUpdatesDrawabox(scope, changedAnnotation); diff --git a/src/components/models/annotation.js b/src/components/models/annotation.js index ed4c4ab5..04591ae3 100644 --- a/src/components/models/annotation.js +++ b/src/components/models/annotation.js @@ -1,22 +1,27 @@ var baw = window.baw = window.baw || {}; -/** - * - * @param localIdOrResource - * @param {*=} audioRecordingId - * @constructor - */ + baw.Annotation = (function () { - // constructor + /** + * + * @param localIdOrResource + * @param {*=} audioRecordingId + * @constructor + */ var module = function Annotation(localIdOrResource, audioRecordingId) { + var localId = typeof(localIdOrResource) === "number" ? localIdOrResource : undefined; var resource; - if (localIdOrResource instanceof Object && localIdOrResource.constructor.name == "Resource") { + if (localIdOrResource instanceof Object /*&& localIdOrResource.constructor.name == "Resource"*/) { resource = localIdOrResource; } + if (!(localId || resource)) { + throw "Valid input not provided"; + } + if (!(this instanceof Annotation)) { throw new Error("Constructor called as a function"); } @@ -37,55 +42,111 @@ baw.Annotation = (function () { this.createdAt = now; this.updatedAt = now; - this.endTimeSeconds = 0.0; - this.highFrequencyHertz = 0.0; - this.isReference = false; - this.lowFrequencyHertz = 0.0; - this.startTimeSeconds = 0.0; - + this._isReference = false; + this.isDirty = true; } // ensure JSON values taken from a resource have nicely formatted values if (resource) { - angular.extend(this, resource); - - this.createdAt = new Date(this.createdAt); - this.updatedAt = new Date(this.updatedAt); - - this.endTimeSeconds = parseFloat(this.endTimeSeconds); - this.highFrequencyHertz = parseFloat(this.highFrequencyHertz); - this.lowFrequencyHertz = parseFloat(this.lowFrequencyHertz); - this.startTimeSeconds = parseFloat(this.startTimeSeconds); - - this.audioEventTags = {}; - angular.forEach(this.audioEventTags, function (value, key) { - this.audioEventTags[key] = new baw.AudioEventTag(value); - }, this); + //angular.extend(this, resource); + + this.id = resource.id; + this.audioRecordingId = resource.audioRecordingId; + this.createdAt = new Date(resource.createdAt); + this.creatorId = resource.creatorId; + this.updatedAt = new Date(resource.updatedAt); + this.updaterId = resource.updaterId; + this.deletedAt = new Date(resource.deletedAt); + this.deleterId = resource.deleterId; + + this._isReference = resource.isReference; + this._endTimeSeconds = parseFloat(resource.endTimeSeconds); + this._highFrequencyHertz = parseFloat(resource.highFrequencyHertz); + this._lowFrequencyHertz = parseFloat(resource.lowFrequencyHertz); + this._startTimeSeconds = parseFloat(resource.startTimeSeconds); + + this.audioEventTags = this.audioEventTags.map(function (value, key) { + return baw.AudioEventTag(value); + }); } }; // strip out unnecessary values; - module.prototype.exportObj = function exportObj() { - return { - // TODO: - taggings: [], - audioRecordingId: this.audioRecordingId, - createdAt: this.createdAt, - endTimeSeconds: this.endTimeSeconds, - highFrequencyHertz: this.highFrequencyHertz, - isReference: this.isReference, - lowFrequencyHertz: this.lowFrequencyHertz, - startTimeSeconds: this.startTimeSeconds, - updatedAt: this.updatedAt, - id: this.id + function prototype() { + // copied from api. Special props defined below. + var pt = { + // from resource + "audioRecordingId": null, + "createdAt": null, + "creatorId": null, + "deletedAt": null, + "deleterId": null, + "id": null, + "updatedAt": null, + "updaterId": null, + "taggings": [ + ] }; - }; - module.prototype.toJSON = function toJSON() { - return { - id: this.id || this.__localId__ + pt._epsilsonDirty = 0.0; + pt.__localId__ = null; + pt.isDirty = false; + + function customProperty(prop) { + var privateProp = "_" + prop; + pt[privateProp] = null; + Object.defineProperty(pt, prop, { + enumerable: true, + configurable: false, + get: function () { + return this[privateProp]; + }, + set: function (value) { + if (value !== this[privateProp]) { + this[privateProp] = value; + this.isDirty = true; + } + } + }); + } + + customProperty("startTimeSeconds"); + customProperty("endTimeSeconds"); + customProperty("highFrequencyHertz"); + customProperty("lowFrequencyHertz"); + customProperty("isReference"); + + pt.isNew = function() { + + return this.__localId__ !== this.id; }; - }; + + pt.exportObj = function exportObj() { + return { + // TODO: + taggings: [], + audioRecordingId: this.audioRecordingId, + createdAt: this.createdAt, + endTimeSeconds: this.endTimeSeconds, + highFrequencyHertz: this.highFrequencyHertz, + isReference: this.isReference, + lowFrequencyHertz: this.lowFrequencyHertz, + startTimeSeconds: this.startTimeSeconds, + updatedAt: this.updatedAt, + id: this.id + }; + }; + + pt.toJSON = function toJSON() { + return { + id: this.id || this.__localId__ + }; + }; + + return pt; + } + + module.prototype = prototype(); module.create = function (arg) { diff --git a/src/components/models/annotation.spec.js b/src/components/models/annotation.spec.js new file mode 100644 index 00000000..28c6cf25 --- /dev/null +++ b/src/components/models/annotation.spec.js @@ -0,0 +1,198 @@ +//noinspection JSUnresolvedFunction +describe("The Annotation object", function () { + + var annotation_local; + var annotation_resource; + var resource = { + "audioRecordingId": 40, + "createdAt": "2013-11-06T03:54:35Z", + "creatorId": 7, + "deletedAt": "2013-11-11T03:54:35Z", + "deleterId": 9, + "endTimeSeconds": 11, + "highFrequencyHertz": 9819, + "id": 1, + "isReference": false, + "lowFrequencyHertz": 5081, + "startTimeSeconds": 7, + "updatedAt": "2013-11-06T03:54:35Z", + "updaterId": 8, + "taggings": [ + ] + }; + + beforeEach(function () { + annotation_local = new baw.Annotation(-1, "audioReadingId"); + annotation_resource = new baw.Annotation(resource); + + }); + + afterEach(function () { + describe("Invariant:", function () { + it("isDirty should be boolean", function () { + expect(annotation_local.isDirty).toBeBoolean(); + expect(annotation_resource.isDirty).toBeBoolean(); + }); + }); + }); + + it("should be found globally", function () { + var type = typeof baw.Annotation; + expect(type).toEqual("function"); + }); + + it("should not be null", function () { + expect(annotation_local).not.toBeNull(); + expect(annotation_resource).not.toBeNull(); + }); + + it("the localId mode to throw if not given a number", function () { + var func = function () { + var annotation = new baw.Annotation(); + }; + + var func2 = function () { + var annotation = new baw.Annotation("30"); + }; + + expect(func).toThrow("Valid input not provided"); + expect(func2).toThrow("Valid input not provided"); + }); + + it("the localId to be negative for a new annotation", function () { + + expect(annotation_local.__localId__).toBeLessThan(0); + expect(annotation_local.id).toBeNull(); + + }); + + it("a new annotation should say so", function () { + + expect(annotation_local.isNew()).toBeTrue(); + }); + + it("the localId to be the same as the id for an existing annotation", function () { + + expect(annotation_resource.id).toBeGreaterThan(0); + expect(annotation_resource.id).toBe(resource.id); + expect(annotation_resource.__localId__).toBe(annotation_resource.id); + + }); + + it("an existing annotation should say so", function () { + + expect(annotation_resource.isNew()).toBeFalse(); + }); + + it("the localId mode to throw if not given a number", function () { + var func = function () { + var annotation = new baw.Annotation(); + }; + + var func2 = function () { + var annotation = new baw.Annotation("30"); + }; + + expect(func).toThrow("Valid input not provided"); + expect(func2).toThrow("Valid input not provided"); + }); + + it("'s prototype should have all of the resource properties defined", function () { + expect(baw.Annotation.prototype).toImplement(resource); + + }); + + var dateFields = [ + "createdAt", + "updatedAt", + "deletedAt"]; + Object.keys(resource).forEach(function (key) { + it("should copy the value of " + key + " into the object", function () { + var expected = (annotation_resource[key]); + var actual = (resource[key]); + + if (dateFields.indexOf(key) >= 0){ + actual = new Date(actual); + expect(expected).toBeDate(actual); + } + else if(key === "taggings") { + // nopop + // TODO: hacky ignore, value not tested + } + else { + expect(expected).toBe(actual); + } + }); + }); + + + it("should have a dirty property", function () { + expect(annotation_local.isDirty).toBeDefined(); + }); + + it("should not be dirty if it has been selected", function () { + annotation_local.isDirty = false; + + annotation_local.selected = true; + expect(annotation_local.isDirty).toBeFalse(); + + annotation_local.selected = false; + expect(annotation_local.isDirty).toBeFalse(); + }); + + it("should be dirty if it has just been created", function () { + expect(annotation_local.isDirty).toBeTrue(); + }); + + it("should not be dirty if it has just been converted from an existing resource", function () { + expect(annotation_resource.isDirty).toBeFalse(); + }); + + describe("bounding boxes dimensions change, then a", function () { + + it("should ensure the properties exist", function () { + expect(annotation_resource.endTimeSeconds).toBeDefined(); + expect(annotation_resource.highFrequencyHertz).toBeDefined(); + expect(annotation_resource.lowFrequencyHertz).toBeDefined(); + expect(annotation_resource.startTimeSeconds).toBeDefined(); + }); + + ["endTimeSeconds", "highFrequencyHertz", "lowFrequencyHertz", "startTimeSeconds"].forEach(function (value) { + it("Δ in " + value + " → a change in the private value _" + value, function () { + var newValue = annotation_resource[value] * (Math.random() + 0.5); + var oldValue = annotation_resource[value]; + var oldPrivateValue = annotation_resource["_" + value]; + + annotation_resource[value] = newValue; + + expect(oldValue).toBe(oldPrivateValue); + + expect(annotation_resource[value]).toBe(newValue); + + expect(annotation_resource["_" + value]).toBe(newValue); + }); + + it("Δ in " + value + " → isDirty ", function () { + annotation_resource[value] = annotation_resource[value] * (Math.random() + 0.5); + + expect(annotation_resource.isDirty).toBeTrue(); + }); + + it("a Δ in " + value + " === 0.0, → !isDirty", function () { + annotation_resource[value] = annotation_resource[value]; + + expect(annotation_resource.isDirty).toBeFalse(); + }); + }); + + + }); + + it("should be dirty if isReference is modified", function() { + expect(annotation_resource.isDirty).toBeFalse(); + annotation_resource.isReference = !annotation_resource.isReference; + expect(annotation_resource.isDirty).toBeTrue(); + }); + + +}); \ No newline at end of file |
---|