From 78f872244bdb68bf96823077cc52dc289e6af5d1 Mon Sep 17 00:00:00 2001 From: Anthony Truskinger Date: Sun, 17 Nov 2013 03:09:30 +1000 Subject: [PATCH] Fixed the event model, got updates working! - Added some packages to make unit testing easier. Loaded by the karma config - added some formatting for the details table on the listen page - implemented isDirty and isNew. Dirty properties are tracked with JavaScript getters/setters - added a ton of unit tests for the Annotation class - the only way I could sort out the nightmare of JS props - Converted some exceptions to assertions in bawAnnotationViewer.js. Also - Also changed unit/converter calculation to work solely off metadata. This means it can be calculated before an image resource is loaded. Images are now stretched to fit. Warnings are still included for badly sized images. - FIxed by in invertPixels function - fleshed out the modelUpdatesServer stub - now with real live updating! w00t! - converted lastUpdate to $lastUpdate - this means the prop is ignored for equality checks. If included the model goes through an extra update loop... very inefficient --- bower.json | 4 +- karma/karma-unit.tpl.js | 14 +- package.json | 68 +++--- src/app/listen/_listen.scss | 12 ++ src/app/listen/listen.tpl.html | 8 +- .../directives/bawAnnotationViewer.js | 133 +++++++----- src/components/models/annotation.js | 153 ++++++++++---- src/components/models/annotation.spec.js | 198 ++++++++++++++++++ 8 files changed, 445 insertions(+), 145 deletions(-) create mode 100644 src/components/models/annotation.spec.js 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 @@

Annotations

- +
+ @@ -156,7 +157,10 @@

Annotations

+
SelectedUnsaved Jump to Annotation ID Audio Recording - 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