From d85d77704086a3350ffc998d6177949757d9313c Mon Sep 17 00:00:00 2001 From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Date: Fri, 19 Jul 2019 18:58:48 +0300 Subject: [PATCH] CVAT.js API Tests (#578) * Added annotations dummy data * Added test file * Weak maps instead of regular dict * Added 14 API tests * Fixed put tests * Some written tests and some additional checks * Merge tests * Split & group tests * Clear annotations tests * Statistics tests * Added frames meta * Selection tests & bug fixes * Tests for frame * ObjectState tests, many fixed bugs * Object state tests * Renamed method FrameData.frame() => FrameData.data() --- cvatjs/src/annotations-collection.js | 55 +- cvatjs/src/annotations-objects.js | 299 ++++- cvatjs/src/annotations-saver.js | 4 +- cvatjs/src/annotations.js | 83 +- cvatjs/src/api.js | 9 +- cvatjs/src/common.js | 4 +- cvatjs/src/frames.js | 10 +- cvatjs/src/object-state.js | 33 +- cvatjs/src/server-proxy.js | 4 +- cvatjs/src/session.js | 19 +- cvatjs/tests/api/annotations.js | 685 ++++++++++ cvatjs/tests/api/frames.js | 71 + cvatjs/tests/api/jobs.js | 8 +- cvatjs/tests/api/object-state.js | 347 +++++ cvatjs/tests/api/plugins.js | 98 ++ cvatjs/tests/api/server.js | 2 +- cvatjs/tests/api/tasks.js | 82 +- cvatjs/tests/api/user.js | 4 +- cvatjs/tests/mocks/dummy-data.mock.js | 1563 ++++++++++++++++++++++- cvatjs/tests/mocks/server-proxy.mock.js | 48 +- 20 files changed, 3190 insertions(+), 238 deletions(-) create mode 100644 cvatjs/tests/api/annotations.js create mode 100644 cvatjs/tests/api/frames.js create mode 100644 cvatjs/tests/api/object-state.js create mode 100644 cvatjs/tests/api/plugins.js diff --git a/cvatjs/src/annotations-collection.js b/cvatjs/src/annotations-collection.js index 6b0ecce37..fc03e7255 100644 --- a/cvatjs/src/annotations-collection.js +++ b/cvatjs/src/annotations-collection.js @@ -104,8 +104,12 @@ } class Collection { - constructor(labels) { - this.labels = labels.reduce((labelAccumulator, label) => { + constructor(data) { + this.startFrame = data.startFrame; + this.stopFrame = data.stopFrame; + this.frameMeta = data.frameMeta; + + this.labels = data.labels.reduce((labelAccumulator, label) => { labelAccumulator[label.id] = label; return labelAccumulator; }, {}); @@ -124,6 +128,7 @@ labels: this.labels, collectionZ: this.collectionZ, groups: this.groups, + frameMeta: this.frameMeta, }; } @@ -208,7 +213,7 @@ const object = this.objects[state.clientID]; if (typeof (object) === 'undefined') { throw new window.cvat.exceptions.ArgumentError( - 'The object has not been saved yet. Call ObjectState.save() before you can merge it', + 'The object has not been saved yet. Call ObjectState.put([state]) before you can merge it', ); } return object; @@ -216,6 +221,18 @@ const keyframes = {}; // frame: position const { label, shapeType } = objectStates[0]; + if (!(label.id in this.labels)) { + throw new window.cvat.exceptions.ArgumentError( + `Unknown label for the task: ${label.id}`, + ); + } + + if (!Object.values(window.cvat.enums.ObjectShape).includes(shapeType)) { + throw new window.cvat.exceptions.ArgumentError( + `Got unknown shapeType "${shapeType}"`, + ); + } + const labelAttributes = label.attributes.reduce((accumulator, attribute) => { accumulator[attribute.id] = attribute; return accumulator; @@ -365,7 +382,9 @@ // Remove other shapes for (const object of objectsForMerge) { object.removed = true; - object.resetCache(); + if (typeof (object.resetCache) === 'function') { + object.resetCache(); + } } } @@ -376,7 +395,7 @@ const object = this.objects[objectState.clientID]; if (typeof (object) === 'undefined') { throw new window.cvat.exceptions.ArgumentError( - 'The object has not been saved yet. Call annotations.put(state) before', + 'The object has not been saved yet. Call annotations.put([state]) before', ); } @@ -385,7 +404,7 @@ } const keyframes = Object.keys(object.shapes).sort((a, b) => +a - +b); - if (frame <= +keyframes[0]) { + if (frame <= +keyframes[0] || frame > keyframes[keyframes.length - 1]) { return; } @@ -463,7 +482,7 @@ const object = this.objects[state.clientID]; if (typeof (object) === 'undefined') { throw new window.cvat.exceptions.ArgumentError( - 'The object has not been saved yet. Call annotations.put(state) before', + 'The object has not been saved yet. Call annotations.put([state]) before', ); } return object; @@ -472,8 +491,12 @@ const groupIdx = reset ? 0 : ++this.groups.max; for (const object of objectsForGroup) { object.group = groupIdx; - object.resetCache(); + if (typeof (object.resetCache) === 'function') { + object.resetCache(); + } } + + return groupIdx; } clear() { @@ -526,7 +549,7 @@ } else if (object instanceof Tag) { objectType = 'tag'; } else { - throw window.cvat.exceptions.ScriptingError( + throw new window.cvat.exceptions.ScriptingError( `Unexpected object type: "${objectType}"`, ); } @@ -543,6 +566,7 @@ if (objectType === 'track') { const keyframes = Object.keys(object.shapes) .sort((a, b) => +a - +b).map(el => +el); + let prevKeyframe = keyframes[0]; let visible = false; @@ -560,6 +584,13 @@ labels[label].total++; } } + + const lastKey = keyframes[keyframes.length - 1]; + if (lastKey !== this.stopFrame && !object.shapes[lastKey].outside) { + const interpolated = this.stopFrame - lastKey; + labels[label].interpolated += interpolated; + labels[label].total += interpolated; + } } else { labels[label].manually++; labels[label].total++; @@ -648,7 +679,7 @@ frame: state.frame, group: 0, label_id: state.label.id, - occluded: state.occluded, + occluded: state.occluded || false, points: [...state.points], type: state.shapeType, z_order: 0, @@ -664,7 +695,7 @@ attributes: attributes .filter(attr => labelAttributes[attr.spec_id].mutable), frame: state.frame, - occluded: state.occluded, + occluded: state.occluded || false, outside: false, points: [...state.points], type: state.shapeType, @@ -698,7 +729,7 @@ const object = this.objects[state.clientID]; if (typeof (object) === 'undefined') { throw new window.cvat.exceptions.ArgumentError( - 'The object has not been saved yet. Call annotations.put(state) before', + 'The object has not been saved yet. Call annotations.put([state]) before', ); } diff --git a/cvatjs/src/annotations-objects.js b/cvatjs/src/annotations-objects.js index 7057c1b55..71fa42cfd 100644 --- a/cvatjs/src/annotations-objects.js +++ b/cvatjs/src/annotations-objects.js @@ -25,6 +25,94 @@ return objectState; } + function checkNumberOfPoints(shapeType, points) { + if (shapeType === window.cvat.enums.ObjectShape.RECTANGLE) { + if (points.length / 2 !== 2) { + throw new window.cvat.exceptions.DataError( + `Rectangle must have 2 points, but got ${points.length / 2}`, + ); + } + } else if (shapeType === window.cvat.enums.ObjectShape.POLYGON) { + if (points.length / 2 < 3) { + throw new window.cvat.exceptions.DataError( + `Polygon must have at least 3 points, but got ${points.length / 2}`, + ); + } + } else if (shapeType === window.cvat.enums.ObjectShape.POLYLINE) { + if (points.length / 2 < 2) { + throw new window.cvat.exceptions.DataError( + `Polyline must have at least 2 points, but got ${points.length / 2}`, + ); + } + } else if (shapeType === window.cvat.enums.ObjectShape.POINTS) { + if (points.length / 2 < 1) { + throw new window.cvat.exceptions.DataError( + `Points must have at least 1 points, but got ${points.length / 2}`, + ); + } + } else { + throw new window.cvat.exceptions.ArgumentError( + `Unknown value of shapeType has been recieved ${shapeType}`, + ); + } + } + + function checkShapeArea(shapeType, points) { + const MIN_SHAPE_LENGTH = 3; + const MIN_SHAPE_AREA = 9; + + if (shapeType === window.cvat.enums.ObjectShape.POINTS) { + return true; + } + + let xmin = Number.MAX_SAFE_INTEGER; + let xmax = Number.MIN_SAFE_INTEGER; + let ymin = Number.MAX_SAFE_INTEGER; + let ymax = Number.MIN_SAFE_INTEGER; + + for (let i = 0; i < points.length - 1; i += 2) { + xmin = Math.min(xmin, points[i]); + xmax = Math.max(xmax, points[i]); + ymin = Math.min(ymin, points[i + 1]); + ymax = Math.max(ymax, points[i + 1]); + } + + if (shapeType === window.cvat.enums.ObjectShape.POLYLINE) { + const length = Math.max( + xmax - xmin, + ymax - ymin, + ); + + return length >= MIN_SHAPE_LENGTH; + } + + const area = (xmax - xmin) * (ymax - ymin); + return area >= MIN_SHAPE_AREA; + } + + function validateAttributeValue(value, attr) { + const { values } = attr; + const type = attr.inputType; + + if (typeof (value) !== 'string') { + throw new window.cvat.exceptions.ArgumentError( + `Attribute value is expected to be string, but got ${typeof (value)}`, + ); + } + + if (type === window.cvat.enums.AttributeType.NUMBER) { + return +value >= +values[0] + && +value <= +values[1] + && !((+value - +values[0]) % +values[2]); + } + + if (type === window.cvat.enums.AttributeType.CHECKBOX) { + return ['true', 'false'].includes(value.toLowerCase()); + } + + return values.includes(value); + } + class Annotation { constructor(data, clientID, injection) { this.taskLabels = injection.labels; @@ -66,10 +154,8 @@ constructor(data, clientID, color, injection) { super(data, clientID, injection); + this.frameMeta = injection.frameMeta; this.collectionZ = injection.collectionZ; - const z = this._getZ(this.frame); - z.max = Math.max(z.max, this.zOrder || 0); - z.min = Math.min(z.min, this.zOrder || 0); this.color = color; this.shapeType = null; @@ -85,19 +171,19 @@ } save() { - throw window.cvat.exceptions.ScriptingError( + throw new window.cvat.exceptions.ScriptingError( 'Is not implemented', ); } get() { - throw window.cvat.exceptions.ScriptingError( + throw new window.cvat.exceptions.ScriptingError( 'Is not implemented', ); } toJSON() { - throw window.cvat.exceptions.ScriptingError( + throw new window.cvat.exceptions.ScriptingError( 'Is not implemented', ); } @@ -123,6 +209,10 @@ this.points = data.points; this.occluded = data.occluded; this.zOrder = data.z_order; + + const z = this._getZ(this.frame); + z.max = Math.max(z.max, this.zOrder || 0); + z.min = Math.min(z.min, this.zOrder || 0); } // Method is used to export data to the server @@ -195,22 +285,47 @@ } if (updated.attributes) { - const labelAttributes = copy.label - .attributes.map(attr => `${attr.id}`); + const labelAttributes = copy.label.attributes + .reduce((accumulator, value) => { + accumulator[value.id] = value; + return accumulator; + }, {}); for (const attrID of Object.keys(data.attributes)) { - if (labelAttributes.includes(attrID)) { - copy.attributes[attrID] = data.attributes[attrID]; + const value = data.attributes[attrID]; + if (attrID in labelAttributes + && validateAttributeValue(value, labelAttributes[attrID])) { + copy.attributes[attrID] = value; + } else { + throw new window.cvat.exceptions.ArgumentError( + `Trying to save unknown attribute with id ${attrID} and value ${value}`, + ); } } } if (updated.points) { checkObjectType('points', data.points, null, Array); - copy.points = []; - for (const coordinate of data.points) { - checkObjectType('coordinate', coordinate, 'number', null); - copy.points.push(coordinate); + checkNumberOfPoints(this.shapeType, data.points); + + // cut points + const { width, height } = this.frameMeta[frame]; + const cutPoints = []; + for (let i = 0; i < data.points.length - 1; i += 2) { + const x = data.points[i]; + const y = data.points[i + 1]; + + checkObjectType('coordinate', x, 'number', null); + checkObjectType('coordinate', y, 'number', null); + + cutPoints.push( + Math.clamp(x, 0, width), + Math.clamp(y, 0, height), + ); + } + + if (checkShapeArea(this.shapeType, cutPoints)) { + copy.points = cutPoints; } } @@ -340,7 +455,6 @@ {}, this.getPosition(frame), { attributes: this.getAttributes(frame), - label: this.label, group: this.group, objectType: window.cvat.enums.ObjectType.TRACK, shapeType: this.shapeType, @@ -354,7 +468,9 @@ this.cache[frame] = interpolation; } - return JSON.parse(JSON.stringify(this.cache[frame])); + const result = JSON.parse(JSON.stringify(this.cache[frame])); + result.label = this.label; + return result; } neighborsFrames(targetFrame) { @@ -408,8 +524,7 @@ } save(frame, data) { - if (this.lock || data.lock) { - this.lock = data.lock; + if (this.lock && data.lock) { return objectStateFactory.call(this, frame, this.get(frame)); } @@ -430,34 +545,50 @@ this.appendDefaultAttributes.call(copy, copy.label); } - if (updated.attributes) { - const labelAttributes = copy.label.attributes - .reduce((accumulator, value) => { - accumulator[value.id] = value; - return accumulator; - }, {}); + const labelAttributes = copy.label.attributes + .reduce((accumulator, value) => { + accumulator[value.id] = value; + return accumulator; + }, {}); + if (updated.attributes) { for (const attrID of Object.keys(data.attributes)) { - if (attrID in labelAttributes) { - copy.attributes[attrID] = data.attributes[attrID]; - if (!labelAttributes[attrID].mutable) { - this.attributes[attrID] = data.attributes[attrID]; - } else { - // Mutable attributes will be updated later - positionUpdated = true; - } + const value = data.attributes[attrID]; + if (attrID in labelAttributes + && validateAttributeValue(value, labelAttributes[attrID])) { + copy.attributes[attrID] = value; + } else { + throw new window.cvat.exceptions.ArgumentError( + `Trying to save unknown attribute with id ${attrID} and value ${value}`, + ); } } } if (updated.points) { checkObjectType('points', data.points, null, Array); - copy.points = []; - for (const coordinate of data.points) { - checkObjectType('coordinate', coordinate, 'number', null); - copy.points.push(coordinate); + checkNumberOfPoints(this.shapeType, data.points); + + // cut points + const { width, height } = this.frameMeta[frame]; + const cutPoints = []; + for (let i = 0; i < data.points.length - 1; i += 2) { + const x = data.points[i]; + const y = data.points[i + 1]; + + checkObjectType('coordinate', x, 'number', null); + checkObjectType('coordinate', y, 'number', null); + + cutPoints.push( + Math.clamp(x, 0, width), + Math.clamp(y, 0, height), + ); + } + + if (checkShapeArea(this.shapeType, cutPoints)) { + copy.points = cutPoints; + positionUpdated = true; } - positionUpdated = true; } if (updated.occluded) { @@ -499,6 +630,11 @@ copy.color = data.color; } + if (updated.keyframe) { + // Just check here + checkObjectType('keyframe', data.keyframe, 'boolean', null); + } + // Commit all changes for (const prop of Object.keys(copy)) { if (prop in this) { @@ -508,8 +644,18 @@ this.cache[frame][prop] = copy[prop]; } + if (updated.attributes) { + // Mutable attributes will be updated below + for (const attrID of Object.keys(copy.attributes)) { + if (!labelAttributes[attrID].mutable) { + this.shapes[frame].attributes[attrID] = data.attributes[attrID]; + this.shapes[frame].attributes[attrID] = data.attributes[attrID]; + } + } + } + if (updated.label) { - for (const shape of this.shapes) { + for (const shape of Object.values(this.shapes)) { shape.attributes = {}; } } @@ -519,7 +665,7 @@ // Remove all cache after this keyframe because it have just become outdated for (const cacheFrame in this.cache) { if (+cacheFrame > frame) { - delete this.cache[frame]; + delete this.cache[cacheFrame]; } } @@ -535,7 +681,7 @@ // Remove all cache after this keyframe because it have just become outdated for (const cacheFrame in this.cache) { if (+cacheFrame > frame) { - delete this.cache[frame]; + delete this.cache[cacheFrame]; } } @@ -552,15 +698,9 @@ }; if (updated.attributes) { - const labelAttributes = this.label.attributes - .reduce((accumulator, value) => { - accumulator[value.id] = value; - return accumulator; - }, {}); - // Unmutable attributes were updated above - for (const attrID of Object.keys(data.attributes)) { - if (attrID in labelAttributes && labelAttributes[attrID].mutable) { + for (const attrID of Object.keys(copy.attributes)) { + if (labelAttributes[attrID].mutable) { this.shapes[frame].attributes[attrID] = data.attributes[attrID]; this.shapes[frame].attributes[attrID] = data.attributes[attrID]; } @@ -743,6 +883,7 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = window.cvat.enums.ObjectShape.RECTANGLE; + checkNumberOfPoints(this.shapeType, this.points); } static distance(points, x, y) { @@ -768,23 +909,25 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = window.cvat.enums.ObjectShape.POLYGON; + checkNumberOfPoints(this.shapeType, this.points); } static distance(points, x, y) { function position(x1, y1, x2, y2) { - return ((x1 - x) * (y2 - y) - (x2 - x) * (y1 - y)); + return ((x2 - x1) * (y - y1) - (x - x1) * (y2 - y1)); } let wn = 0; const distances = []; - for (let i = 0; i < points.length; i += 2) { + + for (let i = 0, j = points.length - 2; i < points.length - 1; j = i, i += 2) { // Current point - const x1 = points[i]; - const y1 = points[i + 1]; + const x1 = points[j]; + const y1 = points[j + 1]; // Next point - const x2 = i + 2 < points.length ? points[i + 2] : points[0]; - const y2 = i + 3 < points.length ? points[i + 3] : points[1]; + const x2 = points[i]; + const y2 = points[i + 1]; // Check if a point is inside a polygon // with a winding numbers algorithm @@ -795,25 +938,31 @@ wn++; } } - } else if (y2 < y) { - if (position(x1, y1, x2, y2) > 0) { + } else if (y2 <= y) { + if (position(x1, y1, x2, y2) < 0) { wn--; } } // Find the shortest distance from point to an edge - if (((x - x1) * (x2 - x)) >= 0 && ((y - y1) * (y2 - y)) >= 0) { - // Find the length of a perpendicular - // https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line - distances.push( - Math.abs((y2 - y1) * x - (x2 - x1) * y + x2 * y1 - y2 * x1) / Math - .sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)), - ); + // Get an equation of a line in general + const aCoef = (y1 - y2); + const bCoef = (x2 - x1); + + // Vector (aCoef, bCoef) is a perpendicular to line + // Now find the point where two lines + // (edge and its perpendicular through the point (x,y)) are cross + const xCross = x - aCoef; + const yCross = y - bCoef; + + if (((xCross - x1) * (x2 - xCross)) >= 0 + && ((yCross - y1) * (y2 - yCross)) >= 0) { + // Cross point is on segment between p1(x1,y1) and p2(x2,y2) + distances.push(Math.sqrt( + Math.pow(x - xCross, 2) + + Math.pow(y - yCross, 2), + )); } else { - // The link below works for lines (which have infinit length) - // There is a case when perpendicular doesn't cross the edge - // In this case we don't use the computed distance - // Instead we use just distance to the nearest point distances.push( Math.min( Math.sqrt(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)), @@ -835,6 +984,7 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = window.cvat.enums.ObjectShape.POLYLINE; + checkNumberOfPoints(this.shapeType, this.points); } static distance(points, x, y) { @@ -878,6 +1028,7 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = window.cvat.enums.ObjectShape.POINTS; + checkNumberOfPoints(this.shapeType, this.points); } static distance(points, x, y) { @@ -899,6 +1050,9 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = window.cvat.enums.ObjectShape.RECTANGLE; + for (const shape of Object.values(this.shapes)) { + checkNumberOfPoints(this.shapeType, shape.points); + } } interpolatePosition(leftPosition, rightPosition, targetFrame) { @@ -1220,7 +1374,7 @@ if (!targetMatched.length) { // Prevent infinity loop - throw window.cvat.exceptions.ScriptingError('Interpolation mapping is empty'); + throw new window.cvat.exceptions.ScriptingError('Interpolation mapping is empty'); } while (!targetMatched.includes(prev)) { @@ -1310,6 +1464,9 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = window.cvat.enums.ObjectShape.POLYGON; + for (const shape of Object.values(this.shapes)) { + checkNumberOfPoints(this.shapeType, shape.points); + } } } @@ -1317,6 +1474,9 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = window.cvat.enums.ObjectShape.POLYLINE; + for (const shape of Object.values(this.shapes)) { + checkNumberOfPoints(this.shapeType, shape.points); + } } } @@ -1324,6 +1484,9 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = window.cvat.enums.ObjectShape.POINTS; + for (const shape of Object.values(this.shapes)) { + checkNumberOfPoints(this.shapeType, shape.points); + } } } diff --git a/cvatjs/src/annotations-saver.js b/cvatjs/src/annotations-saver.js index 55bb100a3..49233646a 100644 --- a/cvatjs/src/annotations-saver.js +++ b/cvatjs/src/annotations-saver.js @@ -102,7 +102,7 @@ } else if (typeof (object.id) === 'undefined') { splitted.created[type].push(object); } else { - throw window.cvat.exceptions.ScriptingError( + throw new window.cvat.exceptions.ScriptingError( `Id of object is defined "${object.id}"` + 'but it absents in initial state', ); @@ -140,7 +140,7 @@ + indexes.shapes.length + indexes.tags.length; if (indexesLength !== savedLength) { - throw window.cvat.exception.ScriptingError( + throw new window.cvat.exception.ScriptingError( 'Number of indexes is differed by number of saved objects' + `${indexesLength} vs ${savedLength}`, ); diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index 61f330f6e..dd8edb6fe 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -11,9 +11,10 @@ const serverProxy = require('./server-proxy'); const Collection = require('./annotations-collection'); const AnnotationsSaver = require('./annotations-saver'); + const { checkObjectType } = require('./common'); - const jobCache = {}; - const taskCache = {}; + const jobCache = new WeakMap(); + const taskCache = new WeakMap(); function getCache(sessionType) { if (sessionType === 'task') { @@ -33,17 +34,32 @@ const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (!(session.id in cache)) { + if (!cache.has(session)) { const rawAnnotations = await serverProxy.annotations .getAnnotations(sessionType, session.id); - const collection = new Collection(session.labels || session.task.labels) - .import(rawAnnotations); + + // Get meta information about frames + const startFrame = sessionType === 'job' ? session.startFrame : 0; + const stopFrame = sessionType === 'job' ? session.stopFrame : session.size - 1; + const frameMeta = {}; + for (let i = startFrame; i <= stopFrame; i++) { + frameMeta[i] = await session.frames.get(i); + } + + const collection = new Collection({ + labels: session.labels || session.task.labels, + startFrame, + stopFrame, + frameMeta, + }).import(rawAnnotations); + const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); - cache[session.id] = { + cache.set(session, { collection, saver, - }; + + }); } } @@ -51,15 +67,15 @@ await getAnnotationsFromServer(session); const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - return cache[session.id].collection.get(frame, filter); + return cache.get(session).collection.get(frame, filter); } async function saveAnnotations(session, onUpdate) { const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (session.id in cache) { - await cache[session.id].saver.save(onUpdate); + if (cache.has(session)) { + await cache.get(session).saver.save(onUpdate); } // If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it @@ -69,11 +85,11 @@ const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (session.id in cache) { - return cache[session.id].collection.merge(objectStates); + if (cache.has(session)) { + return cache.get(session).collection.merge(objectStates); } - throw window.cvat.exceptions.DataError( + throw new window.cvat.exceptions.DataError( 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', ); } @@ -82,11 +98,11 @@ const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (session.id in cache) { - return cache[session.id].collection.split(objectState, frame); + if (cache.has(session)) { + return cache.get(session).collection.split(objectState, frame); } - throw window.cvat.exceptions.DataError( + throw new window.cvat.exceptions.DataError( 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', ); } @@ -95,11 +111,11 @@ const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (session.id in cache) { - return cache[session.id].collection.group(objectStates, reset); + if (cache.has(session)) { + return cache.get(session).collection.group(objectStates, reset); } - throw window.cvat.exceptions.DataError( + throw new window.cvat.exceptions.DataError( 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', ); } @@ -108,23 +124,24 @@ const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (session.id in cache) { - return cache[session.id].saver.hasUnsavedChanges(); + if (cache.has(session)) { + return cache.get(session).saver.hasUnsavedChanges(); } return false; } async function clearAnnotations(session, reload) { + checkObjectType('reload', reload, 'boolean', null); const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (session.id in cache) { - cache[session.id].collection.clear(); + if (cache.has(session)) { + cache.get(session).collection.clear(); } if (reload) { - delete cache[session.id]; + cache.delete(session); await getAnnotationsFromServer(session); } } @@ -133,11 +150,11 @@ const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (session.id in cache) { - return cache[session.id].collection.statistics(); + if (cache.has(session)) { + return cache.get(session).collection.statistics(); } - throw window.cvat.exceptions.DataError( + throw new window.cvat.exceptions.DataError( 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', ); } @@ -146,11 +163,11 @@ const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (session.id in cache) { - return cache[session.id].collection.put(objectStates); + if (cache.has(session)) { + return cache.get(session).collection.put(objectStates); } - throw window.cvat.exceptions.DataError( + throw new window.cvat.exceptions.DataError( 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', ); } @@ -159,11 +176,11 @@ const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (session.id in cache) { - return cache[session.id].collection.select(objectStates, x, y); + if (cache.has(session)) { + return cache.get(session).collection.select(objectStates, x, y); } - throw window.cvat.exceptions.DataError( + throw new window.cvat.exceptions.DataError( 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', ); } diff --git a/cvatjs/src/api.js b/cvatjs/src/api.js index 4f88de8ce..af53cf6f5 100644 --- a/cvatjs/src/api.js +++ b/cvatjs/src/api.js @@ -348,14 +348,12 @@ */ config: { /** + * @memberof module:API.cvat.config * @property {string} backendAPI host with a backend api * @memberof module:API.cvat.config * @property {string} proxy Axios proxy settings. * For more details please read here * @memberof module:API.cvat.config - * @property {integer} preloadFrames the number of subsequent frames which are - * loaded in background - * @memberof module:API.cvat.config * @property {integer} taskID this value is displayed in a logs if available * @memberof module:API.cvat.config * @property {integer} jobID this value is displayed in a logs if available @@ -364,7 +362,6 @@ * value which is displayed in a logs * @memberof module:API.cvat.config */ - preloadFrames: 300, backendAPI: 'http://localhost:7000/api/v1', proxy: false, taskID: undefined, @@ -451,5 +448,9 @@ require('browser-env')(); } + Math.clamp = function (value, min, max) { + return Math.min(Math.max(value, min), max); + }; + window.cvat = Object.freeze(implementAPI(cvat)); })(); diff --git a/cvatjs/src/common.js b/cvatjs/src/common.js index b92d67ff9..dadbbf52f 100644 --- a/cvatjs/src/common.js +++ b/cvatjs/src/common.js @@ -38,7 +38,7 @@ ); } else if (!fields[prop](filter[prop])) { throw new window.cvat.exceptions.ArgumentError( - `Received filter property ${prop} is not satisfied for checker`, + `Received filter property "${prop}" is not satisfied for checker`, ); } } @@ -61,7 +61,7 @@ if (!(value instanceof instance)) { if (value !== undefined) { throw new window.cvat.exceptions.ArgumentError( - `${name} is expected to be ${instance.name}, but ` + `"${name}" is expected to be ${instance.name}, but ` + `"${value.constructor.name}" has been got`, ); } diff --git a/cvatjs/src/frames.js b/cvatjs/src/frames.js index e636eef09..5381fa8a2 100644 --- a/cvatjs/src/frames.js +++ b/cvatjs/src/frames.js @@ -59,7 +59,7 @@ /** * Method returns URL encoded image which can be placed in the img tag - * @method frame + * @method data * @returns {string} * @memberof module:API.cvat.classes.FrameData * @instance @@ -67,16 +67,16 @@ * @throws {module:API.cvat.exception.ServerError} * @throws {module:API.cvat.exception.PluginError} */ - async frame() { + async data() { const result = await PluginRegistry - .apiWrapper.call(this, FrameData.prototype.frame); + .apiWrapper.call(this, FrameData.prototype.data); return result; } } - FrameData.prototype.frame.implementation = async function () { + FrameData.prototype.data.implementation = async function () { if (!(this.number in frameCache[this.tid])) { - const frame = await serverProxy.frames.getFrame(this.tid, this.number); + const frame = await serverProxy.frames.getData(this.tid, this.number); if (window.URL.createObjectURL) { // browser env const url = window.URL.createObjectURL(new Blob([frame])); diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index 75672694b..23c406c01 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -153,12 +153,21 @@ * @name points * @type {number[]} * @memberof module:API.cvat.classes.ObjectState + * @throws {module:API.cvat.exceptions.ArgumentError} * @instance */ get: () => data.points, set: (points) => { - data.updateFlags.points = true; - data.points = [...points]; + if (Array.isArray(points)) { + data.updateFlags.points = true; + data.points = [...points]; + } else { + throw new window.cvat.exceptions.ArgumentError( + 'Points are expected to be an array ' + + `but got ${typeof (points) === 'object' + ? points.constructor.name : typeof (points)}`, + ); + } }, }, group: { @@ -252,14 +261,10 @@ get: () => data.attributes, set: (attributes) => { if (typeof (attributes) !== 'object') { - if (typeof (attributes) === 'undefined') { - throw new window.cvat.exceptions.ArgumentError( - 'Expected attributes are object, but got undefined', - ); - } - throw new window.cvat.exceptions.ArgumentError( - `Expected attributes are object, but got ${attributes.constructor.name}`, + 'Attributes are expected to be an object ' + + `but got ${typeof (attributes) === 'object' + ? attributes.constructor.name : typeof (attributes)}`, ); } @@ -277,11 +282,17 @@ this.outside = serialized.outside; this.keyframe = serialized.keyframe; this.occluded = serialized.occluded; - this.attributes = serialized.attributes; - this.points = serialized.points; this.color = serialized.color; this.lock = serialized.lock; + // It can be undefined in a constructor and it can be defined later + if (typeof (serialized.points) !== 'undefined') { + this.points = serialized.points; + } + if (typeof (serialized.attributes) !== 'undefined') { + this.attributes = serialized.attributes; + } + data.updateFlags.reset(); } diff --git a/cvatjs/src/server-proxy.js b/cvatjs/src/server-proxy.js index a574c434b..086db356e 100644 --- a/cvatjs/src/server-proxy.js +++ b/cvatjs/src/server-proxy.js @@ -421,7 +421,7 @@ return response.data; } - async function getFrame(tid, frame) { + async function getData(tid, frame) { const { backendAPI } = window.cvat.config; let response = null; @@ -629,7 +629,7 @@ frames: { value: Object.freeze({ - getFrame, + getData, getMeta, }), writable: false, diff --git a/cvatjs/src/session.js b/cvatjs/src/session.js index 2ce6380ea..5c764eb45 100644 --- a/cvatjs/src/session.js +++ b/cvatjs/src/session.js @@ -259,6 +259,7 @@ * @param {module:API.cvat.classes.ObjectState[]} data * array of objects on the specific frame * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.DataError} * @throws {module:API.cvat.exceptions.ArgumentError} * @instance * @async @@ -302,11 +303,13 @@ * @async */ /** - * Select shape under a cursor using math alghorithms + * Select shape under a cursor by using minimal distance + * between a cursor and a shape edge or a shape point + * For closed shapes a cursor is placed inside a shape * @method select * @memberof Session.annotations * @param {module:API.cvat.classes.ObjectState[]} objectStates - * object which can be selected + * objects which can be selected * @param {float} x horizontal coordinate * @param {float} y vertical coordinate * @returns {Object} @@ -659,7 +662,7 @@ return this; } - throw window.cvat.exceptions.ArgumentError( + throw new window.cvat.exceptions.ArgumentError( 'Can not save job without and id', ); }; @@ -673,7 +676,7 @@ if (frame < this.startFrame || frame > this.stopFrame) { throw new window.cvat.exceptions.ArgumentError( - `Frame ${frame} does not exist in the job`, + `The frame with number ${frame} is out of the job`, ); } @@ -1262,9 +1265,15 @@ }; Task.prototype.frames.get.implementation = async function (frame) { + if (!Number.isInteger(frame) || frame < 0) { + throw new window.cvat.exceptions.ArgumentError( + `Frame must be a positive integer. Got: "${frame}"`, + ); + } + if (frame >= this.size) { throw new window.cvat.exceptions.ArgumentError( - `Frame ${frame} does not exist in the task`, + `The frame with number ${frame} is out of the task`, ); } diff --git a/cvatjs/tests/api/annotations.js b/cvatjs/tests/api/annotations.js new file mode 100644 index 000000000..6d6f31749 --- /dev/null +++ b/cvatjs/tests/api/annotations.js @@ -0,0 +1,685 @@ +/* + * Copyright (C) 2018 Intel Corporation + * SPDX-License-Identifier: MIT +*/ + +/* global + require:false + jest:false + describe:false +*/ + +// Setup mock for a server +jest.mock('../../src/server-proxy', () => { + const mock = require('../mocks/server-proxy.mock'); + return mock; +}); + +// Initialize api +require('../../src/api'); + +// Test cases +describe('Feature: get annotations', () => { + test('get annotations from a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + expect(Array.isArray(annotations)).toBeTruthy(); + expect(annotations).toHaveLength(11); + for (const state of annotations) { + expect(state).toBeInstanceOf(window.cvat.classes.ObjectState); + } + }); + + test('get annotations from a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 101 }))[0]; + const annotations0 = await job.annotations.get(0); + const annotations10 = await job.annotations.get(10); + expect(Array.isArray(annotations0)).toBeTruthy(); + expect(Array.isArray(annotations10)).toBeTruthy(); + expect(annotations0).toHaveLength(1); + expect(annotations10).toHaveLength(2); + for (const state of annotations0.concat(annotations10)) { + expect(state).toBeInstanceOf(window.cvat.classes.ObjectState); + } + }); + + test('get annotations for frame out of task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + + // Out of task + expect(task.annotations.get(500)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + // Out of task + expect(task.annotations.get(-1)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('get annotations for frame out of job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 101 }))[0]; + + // Out of segment + expect(job.annotations.get(500)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + // Out of segment + expect(job.annotations.get(-1)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + // TODO: Test filter (hasn't been implemented yet) +}); + + +describe('Feature: put annotations', () => { + test('put a shape to a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + let annotations = await task.annotations.get(1); + const { length } = annotations; + + const state = new window.cvat.classes.ObjectState({ + frame: 1, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + label: task.labels[0], + }); + + await task.annotations.put([state]); + annotations = await task.annotations.get(1); + expect(annotations).toHaveLength(length + 1); + }); + + test('put a shape to a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + let annotations = await job.annotations.get(5); + const { length } = annotations; + + const state = new window.cvat.classes.ObjectState({ + frame: 5, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.RECTANGLE, + points: [0, 0, 100, 100], + occluded: false, + label: job.task.labels[0], + }); + + await job.annotations.put([state]); + annotations = await job.annotations.get(5); + expect(annotations).toHaveLength(length + 1); + }); + + test('put a track to a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + let annotations = await task.annotations.get(1); + const { length } = annotations; + + const state = new window.cvat.classes.ObjectState({ + frame: 1, + objectType: window.cvat.enums.ObjectType.TRACK, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + label: task.labels[0], + }); + + await task.annotations.put([state]); + annotations = await task.annotations.get(1); + expect(annotations).toHaveLength(length + 1); + }); + + test('put a track to a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + let annotations = await job.annotations.get(5); + const { length } = annotations; + + const state = new window.cvat.classes.ObjectState({ + frame: 5, + objectType: window.cvat.enums.ObjectType.TRACK, + shapeType: window.cvat.enums.ObjectShape.RECTANGLE, + points: [0, 0, 100, 100], + occluded: false, + label: job.task.labels[0], + }); + + await job.annotations.put([state]); + annotations = await job.annotations.get(5); + expect(annotations).toHaveLength(length + 1); + }); + + test('put object without objectType to a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + await task.annotations.clear(true); + const state = new window.cvat.classes.ObjectState({ + frame: 1, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + label: task.labels[0], + }); + + expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('put shape with bad attributes to a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + await task.annotations.clear(true); + const state = new window.cvat.classes.ObjectState({ + frame: 1, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + attributes: { 'bad key': 55 }, + occluded: true, + label: task.labels[0], + }); + + expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('put shape without points and with invalud points to a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + await task.annotations.clear(true); + const state = new window.cvat.classes.ObjectState({ + frame: 1, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + occluded: true, + points: [], + label: task.labels[0], + }); + + await expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.DataError); + + delete state.points; + await expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.DataError); + + state.points = ['150,50 250,30']; + expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('put shape without type to a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + await task.annotations.clear(true); + const state = new window.cvat.classes.ObjectState({ + frame: 1, + objectType: window.cvat.enums.ObjectType.SHAPE, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + label: task.labels[0], + }); + + expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('put shape without label and with bad label to a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + await task.annotations.clear(true); + const state = new window.cvat.classes.ObjectState({ + frame: 1, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + }); + + await expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + state.label = 'bad label'; + await expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + state.label = {}; + await expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('put shape with bad frame to a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + await task.annotations.clear(true); + const state = new window.cvat.classes.ObjectState({ + frame: '5', + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + label: task.labels[0], + }); + + expect(task.annotations.put([state])) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: check unsaved changes', () => { + test('check unsaved changes in a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + expect(await task.annotations.hasUnsavedChanges()).toBe(false); + const annotations = await task.annotations.get(0); + + annotations[0].keyframe = true; + await annotations[0].save(); + + expect(await task.annotations.hasUnsavedChanges()).toBe(true); + }); + + test('check unsaved changes in a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + expect(await job.annotations.hasUnsavedChanges()).toBe(false); + const annotations = await job.annotations.get(0); + + annotations[0].occluded = true; + await annotations[0].save(); + + expect(await job.annotations.hasUnsavedChanges()).toBe(true); + }); +}); + +describe('Feature: save annotations', () => { + test('create & save annotations for a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + let annotations = await task.annotations.get(0); + const { length } = annotations; + const state = new window.cvat.classes.ObjectState({ + frame: 0, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + label: task.labels[0], + }); + + expect(await task.annotations.hasUnsavedChanges()).toBe(false); + await task.annotations.put([state]); + expect(await task.annotations.hasUnsavedChanges()).toBe(true); + await task.annotations.save(); + expect(await task.annotations.hasUnsavedChanges()).toBe(false); + annotations = await task.annotations.get(0); + expect(annotations).toHaveLength(length + 1); + }); + + test('update & save annotations for a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations = await task.annotations.get(0); + + expect(await task.annotations.hasUnsavedChanges()).toBe(false); + annotations[0].occluded = true; + await annotations[0].save(); + expect(await task.annotations.hasUnsavedChanges()).toBe(true); + await task.annotations.save(); + expect(await task.annotations.hasUnsavedChanges()).toBe(false); + }); + + test('delete & save annotations for a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations = await task.annotations.get(0); + + expect(await task.annotations.hasUnsavedChanges()).toBe(false); + await annotations[0].delete(); + expect(await task.annotations.hasUnsavedChanges()).toBe(true); + await task.annotations.save(); + expect(await task.annotations.hasUnsavedChanges()).toBe(false); + }); + + test('create & save annotations for a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + let annotations = await job.annotations.get(0); + const { length } = annotations; + const state = new window.cvat.classes.ObjectState({ + frame: 0, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + label: job.task.labels[0], + }); + + expect(await job.annotations.hasUnsavedChanges()).toBe(false); + await job.annotations.put([state]); + expect(await job.annotations.hasUnsavedChanges()).toBe(true); + await job.annotations.save(); + expect(await job.annotations.hasUnsavedChanges()).toBe(false); + annotations = await job.annotations.get(0); + expect(annotations).toHaveLength(length + 1); + }); + + test('update & save annotations for a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + const annotations = await job.annotations.get(0); + + expect(await job.annotations.hasUnsavedChanges()).toBe(false); + annotations[0].points = [0, 100, 200, 300]; + await annotations[0].save(); + expect(await job.annotations.hasUnsavedChanges()).toBe(true); + await job.annotations.save(); + expect(await job.annotations.hasUnsavedChanges()).toBe(false); + }); + + test('delete & save annotations for a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + const annotations = await job.annotations.get(0); + + expect(await job.annotations.hasUnsavedChanges()).toBe(false); + await annotations[0].delete(); + expect(await job.annotations.hasUnsavedChanges()).toBe(true); + await job.annotations.save(); + expect(await job.annotations.hasUnsavedChanges()).toBe(false); + }); +}); + +describe('Feature: merge annotations', () => { + test('merge annotations in a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations0 = await task.annotations.get(0); + const annotations1 = await task.annotations.get(1); + const states = [annotations0[0], annotations1[0]]; + await task.annotations.merge(states); + const merged0 = (await task.annotations.get(0)) + .filter(state => state.objectType === window.cvat.enums.ObjectType.TRACK); + const merged1 = (await task.annotations.get(1)) + .filter(state => state.objectType === window.cvat.enums.ObjectType.TRACK); + expect(merged0).toHaveLength(1); + expect(merged1).toHaveLength(1); + + expect(merged0[0].points).toEqual(states[0].points); + expect(merged1[0].points).toEqual(states[1].points); + }); + + test('merge annotations in a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + const annotations0 = await job.annotations.get(0); + const annotations1 = await job.annotations.get(1); + const states = [annotations0[0], annotations1[0]]; + await job.annotations.merge(states); + const merged0 = (await job.annotations.get(0)) + .filter(state => state.objectType === window.cvat.enums.ObjectType.TRACK); + const merged1 = (await job.annotations.get(1)) + .filter(state => state.objectType === window.cvat.enums.ObjectType.TRACK); + expect(merged0).toHaveLength(1); + expect(merged1).toHaveLength(1); + + expect(merged0[0].points).toEqual(states[0].points); + expect(merged1[0].points).toEqual(states[1].points); + }); + + test('trying to merge not object state', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations0 = await task.annotations.get(0); + const states = [annotations0[0], {}]; + + expect(task.annotations.merge(states)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('trying to merge object state which is not saved in a collection', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations0 = await task.annotations.get(0); + + const state = new window.cvat.classes.ObjectState({ + frame: 0, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + label: task.labels[0], + }); + const states = [annotations0[0], state]; + + expect(task.annotations.merge(states)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('trying to merge with bad label', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations0 = await task.annotations.get(0); + const annotations1 = await task.annotations.get(1); + const states = [annotations0[0], annotations1[0]]; + states[0].label = new window.cvat.classes.Label({ + id: 500, + name: 'new_label', + attributes: [], + }); + + expect(task.annotations.merge(states)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('trying to merge with different shape types', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations0 = await task.annotations.get(0); + const annotations1 = (await task.annotations.get(1)) + .filter(state => state.shapeType === window.cvat.enums.ObjectShape.POLYGON); + const states = [annotations0[0], annotations1[0]]; + + expect(task.annotations.merge(states)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('trying to merge with different labels', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations0 = await task.annotations.get(0); + const annotations1 = await task.annotations.get(1); + const states = [annotations0[0], annotations1[0]]; + states[1].label = new window.cvat.classes.Label({ + id: 500, + name: 'new_label', + attributes: [], + }); + + expect(task.annotations.merge(states)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: split annotations', () => { + test('split annotations in a task', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations4 = await task.annotations.get(4); + const annotations5 = await task.annotations.get(5); + + expect(annotations4[0].clientID).toBe(annotations5[0].clientID); + await task.annotations.split(annotations5[0], 5); + const splitted4 = await task.annotations.get(4); + const splitted5 = (await task.annotations.get(5)).filter(state => !state.outside); + expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID); + }); + + test('split annotations in a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 101 }))[0]; + const annotations4 = await job.annotations.get(4); + const annotations5 = await job.annotations.get(5); + + expect(annotations4[0].clientID).toBe(annotations5[0].clientID); + await job.annotations.split(annotations5[0], 5); + const splitted4 = await job.annotations.get(4); + const splitted5 = (await job.annotations.get(5)).filter(state => !state.outside); + expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID); + }); + + test('split on a bad frame', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations4 = await task.annotations.get(4); + const annotations5 = await task.annotations.get(5); + + expect(annotations4[0].clientID).toBe(annotations5[0].clientID); + expect(task.annotations.split(annotations5[0], 'bad frame')) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: group annotations', () => { + test('group annotations in a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + let annotations = await task.annotations.get(0); + const groupID = await task.annotations.group(annotations); + expect(typeof (groupID)).toBe('number'); + annotations = await task.annotations.get(0); + for (const state of annotations) { + expect(state.group).toBe(groupID); + } + }); + + test('group annotations in a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + let annotations = await job.annotations.get(0); + const groupID = await job.annotations.group(annotations); + expect(typeof (groupID)).toBe('number'); + annotations = await job.annotations.get(0); + for (const state of annotations) { + expect(state.group).toBe(groupID); + } + }); + + test('trying to group object state which has not been saved in a collection', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + await task.annotations.clear(true); + + const state = new window.cvat.classes.ObjectState({ + frame: 0, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.POLYGON, + points: [0, 0, 100, 0, 100, 50], + occluded: true, + label: task.labels[0], + }); + + expect(task.annotations.group([state])) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('trying to group not object state', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + expect(task.annotations.group(annotations.concat({}))) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: clear annotations', () => { + test('clear annotations in a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + let annotations = await task.annotations.get(0); + expect(annotations.length).not.toBe(0); + await task.annotations.clear(); + annotations = await task.annotations.get(0); + expect(annotations.length).toBe(0); + }); + + test('clear annotations in a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + let annotations = await job.annotations.get(0); + expect(annotations.length).not.toBe(0); + await job.annotations.clear(); + annotations = await job.annotations.get(0); + expect(annotations.length).toBe(0); + }); + + test('clear annotations with reload in a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + let annotations = await task.annotations.get(0); + expect(annotations.length).not.toBe(0); + annotations[0].occluded = true; + await annotations[0].save(); + expect(await task.annotations.hasUnsavedChanges()).toBe(true); + await task.annotations.clear(true); + annotations = await task.annotations.get(0); + expect(annotations.length).not.toBe(0); + expect(await task.annotations.hasUnsavedChanges()).toBe(false); + }); + + test('clear annotations with reload in a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + let annotations = await job.annotations.get(0); + expect(annotations.length).not.toBe(0); + annotations[0].occluded = true; + await annotations[0].save(); + expect(await job.annotations.hasUnsavedChanges()).toBe(true); + await job.annotations.clear(true); + annotations = await job.annotations.get(0); + expect(annotations.length).not.toBe(0); + expect(await job.annotations.hasUnsavedChanges()).toBe(false); + }); + + test('clear annotations with bad reload parameter', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + await task.annotations.clear(true); + expect(task.annotations.clear('reload')) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: get statistics', () => { + test('get statistics from a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + await task.annotations.clear(true); + const statistics = await task.annotations.statistics(); + expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics); + expect(statistics.total.total).toBe(29); + }); + + test('get statistics from a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 101 }))[0]; + await job.annotations.clear(true); + const statistics = await job.annotations.statistics(); + expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics); + expect(statistics.total.total).toBe(512); + }); +}); + +describe('Feature: select object', () => { + test('select object in a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + let result = await task.annotations.select(annotations, 1430, 765); + expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE); + result = await task.annotations.select(annotations, 1415, 765); + expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON); + expect(result.state.points.length).toBe(10); + result = await task.annotations.select(annotations, 1083, 543); + expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POINTS); + expect(result.state.points.length).toBe(16); + result = await task.annotations.select(annotations, 613, 811); + expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON); + expect(result.state.points.length).toBe(94); + }); + + test('select object in a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + const annotations = await job.annotations.get(0); + let result = await job.annotations.select(annotations, 490, 540); + expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE); + result = await job.annotations.select(annotations, 430, 260); + expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYLINE); + result = await job.annotations.select(annotations, 1473, 250); + expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE); + result = await job.annotations.select(annotations, 1490, 237); + expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON); + expect(result.state.points.length).toBe(94); + }); + + test('trying to select from not object states', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + expect(task.annotations.select(annotations.concat({}), 500, 500)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('trying to select with invalid coordinates', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + expect(task.annotations.select(annotations, null, null)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + expect(task.annotations.select(annotations, null, null)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + expect(task.annotations.select(annotations, '5', '10')) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); +}); diff --git a/cvatjs/tests/api/frames.js b/cvatjs/tests/api/frames.js new file mode 100644 index 000000000..1fb76d18a --- /dev/null +++ b/cvatjs/tests/api/frames.js @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 Intel Corporation + * SPDX-License-Identifier: MIT +*/ + +/* global + require:false + jest:false + describe:false +*/ + +// Setup mock for a server +jest.mock('../../src/server-proxy', () => { + const mock = require('../mocks/server-proxy.mock'); + return mock; +}); + +// Initialize api +require('../../src/api'); + +const { FrameData } = require('../../src/frames'); + +describe('Feature: get frame meta', () => { + test('get meta for a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const frame = await task.frames.get(0); + expect(frame).toBeInstanceOf(FrameData); + }); + + test('get meta for a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + const frame = await job.frames.get(0); + expect(frame).toBeInstanceOf(FrameData); + }); + + test('pass frame number out of a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + expect(task.frames.get(100)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + expect(task.frames.get(-1)) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('pass bad frame number', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + expect(task.frames.get('5')) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('do not pass any frame number', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + expect(task.frames.get()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: get frame data', () => { + test('get frame data for a task', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const frame = await task.frames.get(0); + const frameData = await frame.data(); + expect(typeof (frameData)).toBe('string'); + }); + + test('get frame data for a job', async () => { + const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; + const frame = await job.frames.get(0); + const frameData = await frame.data(); + expect(typeof (frameData)).toBe('string'); + }); +}); diff --git a/cvatjs/tests/api/jobs.js b/cvatjs/tests/api/jobs.js index 6075389a8..9fc48132a 100644 --- a/cvatjs/tests/api/jobs.js +++ b/cvatjs/tests/api/jobs.js @@ -64,26 +64,26 @@ describe('Feature: get a list of jobs', () => { }); test('get jobs by invalid filter with both taskID and jobID', async () => { - await expect(window.cvat.jobs.get({ + expect(window.cvat.jobs.get({ taskID: 1, jobID: 1, })).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); test('get jobs by invalid job id', async () => { - await expect(window.cvat.jobs.get({ + expect(window.cvat.jobs.get({ jobID: '1', })).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); test('get jobs by invalid task id', async () => { - await expect(window.cvat.jobs.get({ + expect(window.cvat.jobs.get({ taskID: '1', })).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); test('get jobs by unknown filter', async () => { - await expect(window.cvat.jobs.get({ + expect(window.cvat.jobs.get({ unknown: 50, })).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); diff --git a/cvatjs/tests/api/object-state.js b/cvatjs/tests/api/object-state.js new file mode 100644 index 000000000..d1dddd830 --- /dev/null +++ b/cvatjs/tests/api/object-state.js @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2018 Intel Corporation + * SPDX-License-Identifier: MIT +*/ + +/* global + require:false + jest:false + describe:false +*/ + +// Setup mock for a server +jest.mock('../../src/server-proxy', () => { + const mock = require('../mocks/server-proxy.mock'); + return mock; +}); + +// Initialize api +require('../../src/api'); + +describe('Feature: set attributes for an object state', () => { + test('set a valid value', () => { + const state = new window.cvat.classes.ObjectState({ + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.RECTANGLE, + frame: 5, + }); + + const attributes = { + 5: 'man', + 6: 'glasses', + }; + + state.attributes = attributes; + expect(state.attributes).toEqual(attributes); + }); + + test('trying to set a bad value', () => { + const state = new window.cvat.classes.ObjectState({ + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.RECTANGLE, + frame: 5, + }); + + let attributes = 'bad attribute'; + expect(() => { + state.attributes = attributes; + }).toThrow(window.cvat.exceptions.ArgumentError); + + attributes = 5; + expect(() => { + state.attributes = attributes; + }).toThrow(window.cvat.exceptions.ArgumentError); + + attributes = false; + expect(() => { + state.attributes = attributes; + }).toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: set points for an object state', () => { + test('set a valid value', () => { + const state = new window.cvat.classes.ObjectState({ + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.RECTANGLE, + frame: 5, + }); + + const points = [1, 2, 3, 4]; + state.points = points; + expect(state.points).toEqual(points); + }); + + test('trying to set a bad value', () => { + const state = new window.cvat.classes.ObjectState({ + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: window.cvat.enums.ObjectShape.RECTANGLE, + frame: 5, + }); + + let points = 'bad points'; + expect(() => { + state.points = points; + }).toThrow(window.cvat.exceptions.ArgumentError); + + points = 5; + expect(() => { + state.points = points; + }).toThrow(window.cvat.exceptions.ArgumentError); + + points = false; + expect(() => { + state.points = points; + }).toThrow(window.cvat.exceptions.ArgumentError); + + points = {}; + expect(() => { + state.points = points; + }).toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: save object from its state', () => { + test('save valid values for a shape', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + let state = annotations[0]; + expect(state.objectType).toBe(window.cvat.enums.ObjectType.SHAPE); + expect(state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE); + state.points = [0, 0, 100, 100]; + state.occluded = true; + [, state.label] = task.labels; + state.lock = true; + state = await state.save(); + expect(state).toBeInstanceOf(window.cvat.classes.ObjectState); + expect(state.label.id).toBe(task.labels[1].id); + expect(state.lock).toBe(true); + expect(state.occluded).toBe(true); + expect(state.points).toEqual([0, 0, 100, 100]); + }); + + test('save valid values for a track', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations = await task.annotations.get(10); + let state = annotations[1]; + expect(state.objectType).toBe(window.cvat.enums.ObjectType.TRACK); + expect(state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE); + + state.occluded = true; + state.lock = true; + state.points = [100, 200, 200, 400]; + state.attributes = { + 1: 'sitting', + 3: 'female', + 2: '10', + 4: 'true', + }; + + state = await state.save(); + expect(state).toBeInstanceOf(window.cvat.classes.ObjectState); + expect(state.lock).toBe(true); + expect(state.occluded).toBe(true); + expect(state.points).toEqual([100, 200, 200, 400]); + expect(state.attributes[1]).toBe('sitting'); + expect(state.attributes[2]).toBe('10'); + expect(state.attributes[3]).toBe('female'); + expect(state.attributes[4]).toBe('true'); + + state.lock = false; + [state.label] = task.labels; + state = await state.save(); + expect(state.label.id).toBe(task.labels[0].id); + + state.outside = true; + state = await state.save(); + expect(state.lock).toBe(false); + expect(state.outside).toBe(true); + + state.keyframe = false; + state = await state.save(); + expect(state.keyframe).toBe(false); + }); + + test('save bad values for a shape', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + const state = annotations[0]; + + state.occluded = 'false'; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + const oldPoints = state.points; + state.occluded = false; + state.points = ['100', '50', '100', {}]; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + state.points = oldPoints; + state.lock = 'true'; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + const oldLabel = state.label; + state.lock = false; + state.label = 1; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + state.label = oldLabel; + state.attributes = { 1: {}, 2: false, 3: () => {} }; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('save bad values for a track', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations = await task.annotations.get(0); + const state = annotations[0]; + + state.occluded = 'false'; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + const oldPoints = state.points; + state.occluded = false; + state.points = ['100', '50', '100', {}]; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + state.points = oldPoints; + state.lock = 'true'; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + const oldLabel = state.label; + state.lock = false; + state.label = 1; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + state.label = oldLabel; + state.outside = 5; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + state.outside = false; + state.keyframe = '10'; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + + state.keyframe = true; + state.attributes = { 1: {}, 2: false, 3: () => {} }; + await expect(state.save()) + .rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('trying to change locked shape', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations = await task.annotations.get(0); + let state = annotations[0]; + + state.lock = true; + state = await state.save(); + + const { points } = state; + state.points = [0, 0, 500, 500]; + state = await state.save(); + expect(state.points).toEqual(points); + }); + + test('trying to set too small area of a shape', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations = await task.annotations.get(0); + let state = annotations[0]; + + const { points } = state; + state.points = [0, 0, 2, 2]; // area is 4 + state = await state.save(); + expect(state.points).toEqual(points); + }); + + test('trying to set too small area of a track', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations = await task.annotations.get(0); + let state = annotations[0]; + + const { points } = state; + state.points = [0, 0, 2, 2]; // area is 4 + state = await state.save(); + expect(state.points).toEqual(points); + }); + + test('trying to set too small length of a shape', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + let state = annotations[8]; + + const { points } = state; + state.points = [0, 0, 2, 2]; // length is 2 + state = await state.save(); + expect(state.points).toEqual(points); + }); +}); + +describe('Feature: delete object', () => { + test('delete a shape', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotationsBefore = await task.annotations.get(0); + const { length } = annotationsBefore; + await annotationsBefore[0].delete(); + const annotationsAfter = await task.annotations.get(0); + expect(annotationsAfter).toHaveLength(length - 1); + }); + + test('delete a track', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotationsBefore = await task.annotations.get(0); + const { length } = annotationsBefore; + await annotationsBefore[0].delete(); + const annotationsAfter = await task.annotations.get(0); + expect(annotationsAfter).toHaveLength(length - 1); + }); +}); + +describe('Feature: change z order of an object', () => { + test('up z order for a shape', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + const state = annotations[0]; + + const { zOrder } = state; + await state.up(); + expect(state.zOrder).toBeGreaterThan(zOrder); + }); + + test('up z order for a track', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations = await task.annotations.get(0); + const state = annotations[0]; + + const { zOrder } = state; + await state.up(); + expect(state.zOrder).toBeGreaterThan(zOrder); + }); + + test('down z order for a shape', async () => { + const task = (await window.cvat.tasks.get({ id: 100 }))[0]; + const annotations = await task.annotations.get(0); + const state = annotations[0]; + + const { zOrder } = state; + await state.down(); + expect(state.zOrder).toBeLessThan(zOrder); + }); + + test('down z order for a track', async () => { + const task = (await window.cvat.tasks.get({ id: 101 }))[0]; + const annotations = await task.annotations.get(0); + const state = annotations[0]; + + const { zOrder } = state; + await state.down(); + expect(state.zOrder).toBeLessThan(zOrder); + }); +}); diff --git a/cvatjs/tests/api/plugins.js b/cvatjs/tests/api/plugins.js new file mode 100644 index 000000000..92ba06c12 --- /dev/null +++ b/cvatjs/tests/api/plugins.js @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2018 Intel Corporation + * SPDX-License-Identifier: MIT +*/ + +/* global + require:false + jest:false + describe:false +*/ + +// Setup mock for a server +jest.mock('../../src/server-proxy', () => { + const mock = require('../mocks/server-proxy.mock'); + return mock; +}); + +// Initialize api +require('../../src/api'); + +describe('Feature: dummy feature', () => { + test('dummy test', async () => { + // TODO: Write test after design of plugin system + }); +}); + +/* +const plugin = { + name: 'Example Plugin', + description: 'This example plugin demonstrates how plugin system in CVAT works', + cvat: { + server: { + about: { + async leave(self, result) { + result.plugins = await self.internal.getPlugins(); + return result; + }, + }, + }, + classes: { + Job: { + prototype: { + annotations: { + put: { + enter(self, objects) { + for (const obj of objects) { + if (obj.type !== 'tag') { + const points = obj.position.map((point) => { + const roundPoint = { + x: Math.round(point.x), + y: Math.round(point.y), + }; + return roundPoint; + }); + obj.points = points; + } + } + }, + }, + }, + }, + }, + }, + }, + internal: { + async getPlugins() { + const plugins = await window.cvat.plugins.list(); + return plugins.map((el) => { + const obj = { + name: el.name, + description: el.description, + }; + return obj; + }); + }, + }, +}; + + +async function test() { + await window.cvat.plugins.register(plugin); + await window.cvat.server.login('admin', 'nimda760'); + + try { + console.log(JSON.stringify(await window.cvat.server.about())); + console.log(await window.cvat.users.get({ self: false })); + console.log(await window.cvat.users.get({ self: true })); + console.log(JSON.stringify(await window.cvat.jobs.get({ taskID: 8 }))); + console.log(JSON.stringify(await window.cvat.jobs.get({ jobID: 10 }))); + console.log(await window.cvat.tasks.get()); + console.log(await window.cvat.tasks.get({ id: 8 })); + console.log('Done.'); + } catch (exception) { + console.log(exception.constructor.name); + console.log(exception.message); + } +} +*/ diff --git a/cvatjs/tests/api/server.js b/cvatjs/tests/api/server.js index 13abc9c45..b6f38c8f2 100644 --- a/cvatjs/tests/api/server.js +++ b/cvatjs/tests/api/server.js @@ -44,7 +44,7 @@ describe('Feature: get share storage info', () => { }); test('get files in a some unknown dir of a share storage', async () => { - await expect(window.cvat.server.share( + expect(window.cvat.server.share( 'Unknown Directory', )).rejects.toThrow(window.cvat.exceptions.ServerError); }); diff --git a/cvatjs/tests/api/tasks.js b/cvatjs/tests/api/tasks.js index 3f317edb3..030ae6e0b 100644 --- a/cvatjs/tests/api/tasks.js +++ b/cvatjs/tests/api/tasks.js @@ -26,7 +26,7 @@ describe('Feature: get a list of tasks', () => { test('get all tasks', async () => { const result = await window.cvat.tasks.get(); expect(Array.isArray(result)).toBeTruthy(); - expect(result).toHaveLength(3); + expect(result).toHaveLength(5); for (const el of result) { expect(el).toBeInstanceOf(Task); } @@ -51,7 +51,7 @@ describe('Feature: get a list of tasks', () => { }); test('get a task by an invalid id', async () => { - await expect(window.cvat.tasks.get({ + expect(window.cvat.tasks.get({ id: '50', })).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); @@ -61,7 +61,7 @@ describe('Feature: get a list of tasks', () => { mode: 'interpolation', }); expect(Array.isArray(result)).toBeTruthy(); - expect(result).toHaveLength(2); + expect(result).toHaveLength(3); for (const el of result) { expect(el).toBeInstanceOf(Task); expect(el.mode).toBe('interpolation'); @@ -69,7 +69,7 @@ describe('Feature: get a list of tasks', () => { }); test('get tasks by invalid filters', async () => { - await expect(window.cvat.tasks.get({ + expect(window.cvat.tasks.get({ unknown: '5', })).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); @@ -184,77 +184,3 @@ describe('Feature: delete a task', () => { expect(result).toHaveLength(0); }); }); - - -/* -const plugin = { - name: 'Example Plugin', - description: 'This example plugin demonstrates how plugin system in CVAT works', - cvat: { - server: { - about: { - async leave(self, result) { - result.plugins = await self.internal.getPlugins(); - return result; - }, - }, - }, - classes: { - Job: { - prototype: { - annotations: { - put: { - enter(self, objects) { - for (const obj of objects) { - if (obj.type !== 'tag') { - const points = obj.position.map((point) => { - const roundPoint = { - x: Math.round(point.x), - y: Math.round(point.y), - }; - return roundPoint; - }); - obj.points = points; - } - } - }, - }, - }, - }, - }, - }, - }, - internal: { - async getPlugins() { - const plugins = await window.cvat.plugins.list(); - return plugins.map((el) => { - const obj = { - name: el.name, - description: el.description, - }; - return obj; - }); - }, - }, -}; - - -async function test() { - await window.cvat.plugins.register(plugin); - await window.cvat.server.login('admin', 'nimda760'); - - try { - console.log(JSON.stringify(await window.cvat.server.about())); - console.log(await window.cvat.users.get({ self: false })); - console.log(await window.cvat.users.get({ self: true })); - console.log(JSON.stringify(await window.cvat.jobs.get({ taskID: 8 }))); - console.log(JSON.stringify(await window.cvat.jobs.get({ jobID: 10 }))); - console.log(await window.cvat.tasks.get()); - console.log(await window.cvat.tasks.get({ id: 8 })); - console.log('Done.'); - } catch (exception) { - console.log(exception.constructor.name); - console.log(exception.message); - } -} -*/ diff --git a/cvatjs/tests/api/user.js b/cvatjs/tests/api/user.js index 9b098c1b7..d763ebd2e 100644 --- a/cvatjs/tests/api/user.js +++ b/cvatjs/tests/api/user.js @@ -41,13 +41,13 @@ describe('Feature: get a list of users', () => { }); test('get users with unknown filter key', async () => { - await expect(window.cvat.users.get({ + expect(window.cvat.users.get({ unknown: '50', })).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); test('get users with invalid filter key', async () => { - await expect(window.cvat.users.get({ + expect(window.cvat.users.get({ self: 1, })).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); diff --git a/cvatjs/tests/mocks/dummy-data.mock.js b/cvatjs/tests/mocks/dummy-data.mock.js index 75498b1d9..065b34e6c 100644 --- a/cvatjs/tests/mocks/dummy-data.mock.js +++ b/cvatjs/tests/mocks/dummy-data.mock.js @@ -106,10 +106,436 @@ const shareDummyData = [ ] const tasksDummyData = { - "count": 3, + "count": 4, "next": null, "previous": null, "results": [ + { + "url": "http://localhost:7000/api/v1/tasks/1", + "id": 100, + "name": "Image Task", + "size": 9, + "mode": "annotation", + "owner": 1, + "assignee": null, + "bug_tracker": "", + "created_date": "2019-06-18T13:05:08.941304+03:00", + "updated_date": "2019-07-16T15:51:29.142871+03:00", + "overlap": 0, + "segment_size": 0, + "z_order": false, + "status": "annotation", + "labels": [ + { + "id": 1, + "name": "car,", + "attributes": [ + + ] + }, + { + "id": 2, + "name": "person", + "attributes": [ + + ] + } + ], + "segments": [ + { + "start_frame": 0, + "stop_frame": 8, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/1", + "id": 100, + "assignee": null, + "status": "annotation" + } + ] + } + ], + "image_quality": 50, + "start_frame": 0, + "stop_frame": 0, + "frame_filter": "" + }, + { + "url": "http://localhost:7000/api/v1/tasks/10", + "id": 101, + "name": "Video Task", + "size": 5002, + "mode": "interpolation", + "owner": 1, + "assignee": null, + "bug_tracker": "", + "created_date": "2019-06-21T16:34:49.199691+03:00", + "updated_date": "2019-07-12T16:43:58.904892+03:00", + "overlap": 5, + "segment_size": 500, + "z_order": false, + "status": "annotation", + "labels": [ + { + "id": 22, + "name": "bicycle", + "attributes":[ + { + "id": 13, + "name": "driver", + "mutable": false, + "input_type": "radio", + "default_value": "man", + "values": [ + "man", + "woman" + ] + }, + { + "id": 14, + "name": "sport", + "mutable": true, + "input_type": "checkbox", + "default_value": "false", + "values": [ + "false" + ] + } + ] + }, + { + "id": 21, + "name": "car", + "attributes": [ + { + "id": 10, + "name": "model", + "mutable": false, + "input_type": "select", + "default_value": "__undefined__", + "values": [ + "__undefined__", + "bmw", + "mazda", + "suzuki", + "kia" + ] + }, + { + "id": 11, + "name": "driver", + "mutable": false, + "input_type": "select", + "default_value": "__undefined__", + "values": [ + "__undefined__", + "man", + "woman" + ] + }, + { + "id": 12, + "name": "parked", + "mutable": true, + "input_type": "checkbox", + "default_value": "true", + "values": [ + "true" + ] + } + ] + }, + { + "id": 20, + "name": "face", + "attributes": [ + { + "id": 6, + "name": "age", + "mutable": false, + "input_type": "select", + "default_value": "__undefined__", + "values": [ + "__undefined__", + "skip", + "baby (0-5)", + "child (6-12)", + "adolescent (13-19)", + "adult (20-45)", + "middle-age (46-64)", + "old (65-)" + ] + }, + { + "id": 7, + "name": "glass", + "mutable": false, + "input_type": "select", + "default_value": "__undefined__", + "values": [ + "__undefined__", + "skip", + "no", + "sunglass", + "transparent", + "other" + ] + }, + { + "id": 8, + "name": "beard", + "mutable": false, + "input_type": "select", + "default_value": "__undefined__", + "values": [ + "__undefined__", + "skip", + "no", + "yes" + ] + }, + { + "id": 9, + "name": "race", + "mutable": false, + "input_type": "select", + "default_value": "__undefined__", + "values": [ + "__undefined__", + "skip", + "asian", + "black", + "caucasian", + "other" + ] + } + ] + }, + { + "id": 23, + "name": "motorcycle", + "attributes": [ + { + "id": 15, + "name": "model", + "mutable": false, + "input_type": "text", + "default_value": "unknown", + "values": [ + "unknown" + ] + } + ] + }, + { + "id": 19, + "name": "person, pedestrian", + "attributes": [ + { + "id": 1, + "name": "action", + "mutable": true, + "input_type": "select", + "default_value": "__undefined__", + "values": [ + "__undefined__", + "sitting", + "raising_hand", + "standing" + ] + }, + { + "id": 2, + "name": "age", + "mutable": false, + "input_type": "number", + "default_value": "1", + "values": [ + "1", + "100", + "1" + ] + }, + { + "id": 3, + "name": "gender", + "mutable" :false, + "input_type": "select", + "default_value": "male", + "values": [ + "male", + "female" + ] + }, + { + "id": 4, + "name": "false positive", + "mutable": false, + "input_type": "checkbox", + "default_value": "false", + "values": [ + "false" + ] + }, + { + "id": 5, + "name": "clother", + "mutable": true, + "input_type": "text", + "default_value": "non, initialized", + "values": [ + "non, initialized" + ] + } + ] + }, + { + "id": 24, + "name": "road", + "attributes": [ + + ] + } + ], + "segments": [ + { + "start_frame": 0, + "stop_frame": 499, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/10", + "id": 101, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 495, + "stop_frame": 994, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/11", + "id": 102, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 990, + "stop_frame": 1489, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/12", + "id": 103, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 1485, + "stop_frame": 1984, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/13", + "id": 104, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 1980, + "stop_frame": 2479, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/14", + "id": 105, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 2475, + "stop_frame": 2974, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/15", + "id": 106, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 2970, + "stop_frame": 3469, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/16", + "id": 107, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 3465, + "stop_frame": 3964, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/17", + "id": 108, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 3960, + "stop_frame": 4459, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/18", + "id": 109, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 4455, + "stop_frame": 4954, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/19", + "id": 110, + "assignee": null, + "status": "annotation" + } + ] + }, + { + "start_frame": 4950, + "stop_frame": 5001, + "jobs": [ + { + "url": "http://localhost:7000/api/v1/jobs/20", + "id": 111, + "assignee": null, + "status": "annotation" + } + ] + } + ], + "image_quality": 50, + "start_frame": 0, + "stop_frame": 5001, + "frame_filter": "" + }, { "url": "http://localhost:7000/api/v1/tasks/3", "id": 3, @@ -881,9 +1307,1142 @@ const tasksDummyData = { ] } +const taskAnnotationsDummyData = { + '101': { + "version":21, + "tags":[], + "shapes":[], + "tracks":[ + { + "id": 25, // interpolation + "frame": 10, + "label_id": 19, + "group": 0, + "shapes": [{ + "type": "polygon", + "occluded": false, + "z_order": 2, + "points": [ + 377.64912280702083, + 458.5473684210556, + 383.82456140351314, + 458.5473684210556, + 406.98245614035477, + 455.45964912281124, + 431.6842105263204, + 455.45964912281124, + 457.92982456140817, + 455.45964912281124, + 482.6315789473738, + 455.45964912281124, + 508.87719298246157, + 455.45964912281124, + 535.1228070175493, + 455.45964912281124, + 559.8245614035113, + 455.45964912281124, + 587.6140350877249, + 455.45964912281124, + 620.0350877193014, + 455.45964912281124, + 640.1052631578968, + 455.45964912281124, + 664.8070175438625, + 453.9157894736891, + 692.5964912280724, + 450.8280701754411, + 721.9298245614082, + 450.8280701754411, + 743.5438596491258, + 447.74035087719676, + 769.7894736842136, + 446.1964912280746, + 796.0350877193014, + 446.1964912280746, + 823.8245614035113, + 446.1964912280746, + 846.9824561403548, + 446.1964912280746, + 876.3157894736869, + 446.1964912280746, + 905.6491228070226, + 446.1964912280746, + 931.8947368421104, + 446.1964912280746, + 959.6842105263204, + 446.1964912280746, + 987.4736842105303, + 446.1964912280746, + 1015.2631578947403, + 446.1964912280746, + 1039.964912280706, + 446.1964912280746, + 1066.2105263157937, + 446.1964912280746, + 1090.9122807017593, + 446.1964912280746, + 1115.614035087725, + 446.1964912280746, + 1138.7719298245647, + 449.28421052631893, + 1231.4000000000015, + 413.8000000000011, + 1180.4561403508815, + 467.81052631579223, + 1180.4561403508815, + 494.05614035088, + 1180.4561403508815, + 520.3017543859678, + 1180.4561403508815, + 545.0035087719334, + 1180.4561403508815, + 571.2491228070212, + 1180.4561403508815, + 597.494736842109, + 1180.4561403508815, + 620.6526315789524, + 1180.4561403508815, + 649.9859649122845, + 1180.4561403508815, + 676.2315789473723, + 1180.4561403508815, + 699.3894736842158, + 1180.4561403508815, + 727.1789473684257, + 1180.4561403508815, + 747.2491228070212, + 1180.4561403508815, + 771.9508771929868, + 1180.4561403508815, + 802.8280701754411, + 1180.4561403508815, + 830.6175438596547, + 1180.4561403508815, + 853.7754385964945, + 1180.4561403508815, + 880.0210526315823, + 1183.5438596491258, + 901.6350877193036, + 1183.5438596491258, + 929.4245614035135, + 1186.6315789473738, + 952.5824561403533, + 1188.175438596496, + 975.7403508771968, + 1188.175438596496, + 1001.9859649122845, + 1188.175438596496, + 1023.6000000000022, + 1188.175438596496, + 1057.5649122807044, + 1186.6315789473738, + 1082.26666666667, + 1186.6315789473738, + 1108.5122807017578, + 1186.6315789473738, + 1133.2140350877235, + 1175.82421875, + 1154.828125, + 1155.7543859649159, + 1156.371929824567, + 1132.5964912280724, + 1154.828070175441, + 1106.3508771929846, + 1154.828070175441, + 1078.5614035087747, + 1154.828070175441, + 1053.8596491228127, + 1150.1964912280746, + 1030.7017543859693, + 1148.6526315789524, + 1002.9122807017593, + 1148.6526315789524, + 982.8421052631602, + 1148.6526315789524, + 953.5087719298281, + 1147.1087719298303, + 922.6315789473738, + 1147.1087719298303, + 891.7543859649159, + 1147.1087719298303, + 868.5964912280724, + 1147.1087719298303, + 839.2631578947403, + 1147.1087719298303, + 816.1052631578968, + 1147.1087719298303, + 786.7719298245647, + 1147.1087719298303, + 760.5263157894769, + 1147.1087719298303, + 735.8245614035113, + 1147.1087719298303, + 708.0350877193014, + 1142.47719298246, + 684.8771929824616, + 1140.933333333338, + 658.6315789473738, + 1140.933333333338, + 633.9298245614082, + 1140.933333333338, + 607.6842105263204, + 1139.3894736842158, + 581.4385964912326, + 1134.7578947368456, + 559.8245614035113, + 1133.2140350877235, + 535.1228070175493, + 1131.6701754386013, + 505.7894736842136, + 1131.6701754386013, + 482.6315789473738, + 1131.6701754386013, + 454.8421052631602, + 1130.1263157894791, + 430.1403508771964, + 1130.1263157894791, + 405.4385964912326, + 1130.1263157894791, + 383.82421875, + 1130.126953125, + 382.28070175438916, + 1113.143859649128, + 380.736842105267, + 1088.4421052631624, + 380.736842105267, + 1056.0210526315823, + 380.736842105267, + 1026.6877192982502, + 379.1929824561448, + 1005.0736842105289, + 374.5614035087765, + 978.8280701754411, + 371.47368421053034, + 949.494736842109, + 371.47368421053034, + 921.705263157899, + 371.47368421053034, + 897.0035087719334, + 371.47368421053034, + 866.1263157894791, + 371.47368421053034, + 842.9684210526357, + 371.47368421053034, + 810.5473684210556, + 371.47368421053034, + 778.1263157894791, + 377.64912280702083, + 751.8807017543913, + 380.736842105267, + 722.5473684210556, + 385.3684210526353, + 693.2140350877235, + 385.3684210526353, + 668.5122807017578, + 386.9122807017575, + 643.8105263157922, + 388.45614035088147, + 619.1087719298266, + 388.45614035088147, + 591.3192982456167, + 388.45614035088147, + 563.5298245614067, + 388.45614035088147, + 535.7403508771968, + 388.45614035088147, + 511.03859649123115, + 386.9122807017575, + 487.88070175439134 + ], + "id":382, + "frame":10, + "outside":false, + "attributes": [{ + "spec_id":1, + "value":"__undefined__" + }, { + "spec_id":5, + "value":"non, initialized" + }] + }, { + "type": "polygon", + "occluded": false, + "z_order": 2, + "points": [ + 502.701171875, + 1093.07421875, + 860.8771929824616, + 443.10877192982844, + 1462.9824561403548, + 1120.8631578947425 + ], + "id": 383, + "frame": 20, + "outside": false, + "attributes": [] + }, { + "type": "polygon", + "occluded": false, + "z_order": 2, + "points": [ + 502.701171875, + 1093.07421875, + 860.8771929824616, + 443.10877192982844, + 1462.9824561403548, + 1120.8631578947425 + ], + "id": 384, + "frame": 22, + "outside": true, + "attributes": [] + }], + "attributes": [{ + "spec_id": 2, + "value": "1" + }, { + "spec_id": 3, + "value": "male" + }, { + "spec_id": 4, + "value": "false" + }] + }, + { + "id": 60, + "frame": 0, + "label_id": 19, + "group": 0, + "shapes": [{ + "type": "rectangle", + "occluded": false, + "z_order": 1, + "points": [ + 425.58984375, + 540.298828125, + 755.9765625, + 745.6328125 + ], + "id": 379, + "frame": 0, + "outside": false, + "attributes": [ + { + "spec_id":5, + "value":"non, initialized" + }, + { + "spec_id":1, + "value":"__undefined__" + } + ] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 1, + "points": [ + 238.8000000000011, + 498.6000000000022, + 546.01171875, + 660.720703125 + ], + "id": 380, + "frame": 10, + "outside": false, + "attributes": [] + }, { + "type":"rectangle", + "occluded":false, + "z_order":1, + "points":[ + 13.3955078125, + 447.650390625, + 320.6072265624989, + 609.7710937499978 + ], + "id":381, + "frame":20, + "outside":false, + "attributes":[ + + ] + }], + "attributes":[ + { + "spec_id":2, + "value":"1" + }, + { + "spec_id":3, + "value":"male" + }, + { + "spec_id":4, + "value":"false" + }] + } + ] + }, + '100': { + "version": 16, + "tags": [], + "shapes": [{ + "type": "rectangle", + "occluded": false, + "z_order": 1, + "points": [ + 387.91, + 403.81, + 595.14, + 712.25 + ], + "id": 108, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 2, + "points": [ + 783.12, + 368.91, + 990.35, + 677.34 + ], + "id": 109, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 3, + "points": [ + 1277.1, + 239.99, + 1484.33, + 548.43 + ], + "id": 110, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 4, + "points": [ + 1420.48, + 713.49, + 1627.71, + 1021.92 + ], + "id": 111, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes":[ + + ] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 5, + "points": [ + 896.38, + 659.27, + 1103.61, + 967.71 + ], + "id": 112, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "polygon", + "occluded": false, + "z_order": 6, + "points": [ + 449.36, + 892.97, + 449.36, + 892.97, + 468.63, + 913.46, + 495.14, + 933.94, + 527.67, + 955.62, + 562.61, + 973.7, + 589.12, + 983.34, + 613.21, + 988.15, + 632.49, + 991.77, + 656.59, + 994.18, + 686.71, + 994.18, + 733.69, + 980.93, + 772.25, + 959.24, + 809.6, + 927.91, + 837.31, + 896.59, + 851.77, + 867.67, + 861.41, + 841.17, + 862.61, + 805.02, + 840.92, + 759.24, + 802.37, + 720.68, + 777.07, + 703.82, + 750.56, + 690.56, + 726.47, + 684.54, + 698.75, + 680.92, + 681.89, + 680.92, + 656.59, + 680.92, + 633.69, + 683.33, + 608.39, + 690.56, + 578.27, + 706.22, + 548.15, + 718.27, + 518.03, + 730.32, + 486.71, + 743.57, + 458.99, + 756.83, + 434.9, + 766.47, + 408.39, + 777.31, + 381.89, + 786.95, + 354.17, + 794.18, + 331.28, + 800.2, + 295.14, + 803.82, + 283.09, + 800.2, + 267.43, + 783.33, + 255.38, + 766.47, + 232.49, + 733.94, + 220.44, + 713.45, + 212.0, + 688.15, + 208.39, + 666.47, + 210.8, + 647.19 + ], + "id": 113, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes":[] + }, { + "type": "polygon", + "occluded": false, + "z_order": 7, + "points": [ + 1260.84, + 344.81, + 1260.84, + 344.81, + 1280.11, + 365.29, + 1306.62, + 385.78, + 1339.15, + 407.46, + 1374.09, + 425.53, + 1400.6, + 435.17, + 1424.69, + 439.99, + 1443.97, + 443.61, + 1468.07, + 446.02, + 1498.19, + 446.02, + 1545.18, + 432.76, + 1583.73, + 411.08, + 1621.08, + 379.75, + 1648.79, + 348.43, + 1663.25, + 319.51, + 1672.89, + 293.0, + 1674.09, + 256.86, + 1652.41, + 211.08, + 1613.85, + 172.52, + 1588.55, + 155.65, + 1562.04, + 142.4, + 1537.95, + 136.38, + 1510.24, + 132.76, + 1493.37, + 132.76, + 1468.07, + 132.76, + 1445.18, + 135.17, + 1419.87, + 142.4, + 1389.75, + 158.06, + 1359.63, + 170.11, + 1329.51, + 182.16, + 1298.19, + 195.41, + 1270.48, + 208.67, + 1246.38, + 218.3, + 1219.87, + 229.15, + 1193.37, + 238.79, + 1165.66, + 246.02, + 1142.76, + 252.04, + 1106.62, + 255.65, + 1094.57, + 252.04, + 1078.91, + 235.17, + 1066.86, + 218.3, + 1043.97, + 185.77, + 1031.92, + 165.29, + 1023.49, + 139.99, + 1019.87, + 118.3, + 1022.28, + 99.03 + ], + "id": 114, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "polygon", + "occluded": false, + "z_order": 8, + "points": [ + 1113.21, + 723.09, + 1322.86, + 1018.28, + 1562.62, + 873.7, + 1587.92, + 641.16, + 1267.43, + 530.32 + ], + "id": 115, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "polyline", + "occluded": false, + "z_order": 9, + "points": [ + 268.63, + 359.23, + 277.07, + 344.78, + 292.73, + 325.5, + 312.01, + 311.04, + 331.28, + 300.2, + 349.36, + 295.38, + 375.86, + 290.56, + 387.91, + 290.56, + 418.03, + 290.56, + 439.72, + 292.97, + 457.79, + 295.38, + 492.73, + 301.4, + 525.26, + 306.22, + 534.9, + 306.22, + 571.04, + 296.58, + 591.53, + 284.54, + 610.8, + 272.49, + 640.92, + 253.21, + 655.38, + 238.75 + ], + "id": 116, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "points", + "occluded": false, + "z_order": 10, + "points": [ + 1089.12, + 505.02, + 1178.28, + 543.57, + 1074.66, + 602.61, + 1109.6, + 680.92, + 1172.25, + 631.53, + 1036.11, + 576.1, + 1057.79, + 445.98, + 1185.51, + 400.2 + ], + "id": 117, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes": [ + + ] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 11, + "points": [ + 1565.03, + 555.62, + 1787.92, + 765.26 + ], + "id": 118, + "frame": 0, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 100.0, + 100.0 + ], + "id": 119, + "frame": 1, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "polygon", + "occluded": false, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 200.0, + 0.0, + 100.0, + 200.0 + ], + "id": 120, + "frame": 1, + "label_id": 2, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 11, + "points": [ + 1211.6, + 500.48, + 1434.49, + 710.12 + ], + "id": 121, + "frame": 1, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": true, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 200.0, + 200.0 + ], + "id": 122, + "frame": 1, + "label_id": 2, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 11, + "points": [ + 1211.6, + 500.48, + 1434.49, + 710.12 + ], + "id": 123, + "frame": 2, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": true, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 200.0, + 200.0 + ], + "id": 124, + "frame": 2, + "label_id": 2, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 11, + "points": [ + 1211.6, + 500.48, + 1434.49, + 710.12 + ], + "id": 125, + "frame": 3, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": true, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 200.0, + 200.0 + ], + "id": 126, + "frame": 3, + "label_id": 2, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 11, + "points": [ + 1211.6, + 500.48, + 1434.49, + 710.12 + ], + "id": 127, + "frame": 4, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": true, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 200.0, + 200.0 + ], + "id": 128, + "frame": 4, + "label_id": 2, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 11, + "points": [ + 1211.6, + 500.48, + 1434.49, + 710.12 + ], + "id": 129, + "frame": 5, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": true, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 200.0, + 200.0 + ], + "id": 130, + "frame": 5, + "label_id": 2, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 11, + "points": [ + 1211.6, + 500.48, + 1434.49, + 710.12 + ], + "id": 131, + "frame": 6, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": true, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 200.0, + 200.0 + ], + "id": 132, + "frame": 6, + "label_id": 2, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 11, + "points": [ + 1211.6, + 500.48, + 1434.49, + 710.12 + ], + "id": 133, + "frame": 7, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": true, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 200.0, + 200.0 + ], + "id": 134, + "frame": 7, + "label_id": 2, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": false, + "z_order": 11, + "points": [ + 1211.6, + 500.48, + 1434.49, + 710.12 + ], + "id": 135, + "frame": 8, + "label_id": 1, + "group": 0, + "attributes": [] + }, { + "type": "rectangle", + "occluded": true, + "z_order": 0, + "points": [ + 0.0, + 0.0, + 200.0, + 200.0 + ], + "id": 136, + "frame": 8, + "label_id": 2, + "group": 0, + "attributes": [] + }], + "tracks":[] + } +}; + +const jobAnnotationsDummyData = JSON.parse(JSON.stringify(taskAnnotationsDummyData)); + +const frameMetaDummyData = { + 1: [{ + "width": 1920, + "height": 1080 + }, { + "width": 1600, + "height": 1143 + }, { + "width": 1600, + "height": 859 + }, { + "width": 3840, + "height": 2160 + }, { + "width": 2560, + "height": 1920 + }, { + "width": 1920, + "height": 1080 + }, { + "width": 1920, + "height": 1080 + }, { + "width": 700, + "height": 453 + }, { + "width": 1920, + "height": 1200 + }], + 2: [{ + "width": 1920, + "height": 1080 + }], + 3: [{ + "width": 1888, + "height": 1408 + }], + 100: [{ + "width": 1920, + "height": 1080 + }, { + "width": 1600, + "height": 1143 + }, { + "width": 1600, + "height": 859 + }, { + "width": 3840, + "height": 2160 + }, { + "width": 2560, + "height": 1920 + }, { + "width": 1920, + "height": 1080 + }, { + "width": 1920, + "height": 1080 + }, { + "width": 700, + "height": 453 + }, { + "width": 1920, + "height": 1200 + }], + 101: [{ + "width": 1888, + "height": 1408 + }], +} + module.exports = { tasksDummyData, aboutDummyData, shareDummyData, usersDummyData, -} \ No newline at end of file + taskAnnotationsDummyData, + jobAnnotationsDummyData, + frameMetaDummyData, +} diff --git a/cvatjs/tests/mocks/server-proxy.mock.js b/cvatjs/tests/mocks/server-proxy.mock.js index 5b5f0e5e8..e18a3db19 100644 --- a/cvatjs/tests/mocks/server-proxy.mock.js +++ b/cvatjs/tests/mocks/server-proxy.mock.js @@ -14,6 +14,9 @@ const { aboutDummyData, shareDummyData, usersDummyData, + taskAnnotationsDummyData, + jobAnnotationsDummyData, + frameMetaDummyData, } = require('./dummy-data.mock'); @@ -185,19 +188,50 @@ class ServerProxy { return JSON.parse(JSON.stringify(usersDummyData)).results[0]; } - async function getFrame() { - return null; + async function getData() { + return 'DUMMY_IMAGE'; } - async function getMeta() { - return null; + async function getMeta(tid) { + return JSON.parse(JSON.stringify(frameMetaDummyData[tid])); } - async function getAnnotations() { + async function getAnnotations(session, id) { + if (session === 'task') { + return JSON.parse(JSON.stringify(taskAnnotationsDummyData[id])); + } + + if (session === 'job') { + return JSON.parse(JSON.stringify(jobAnnotationsDummyData[id])); + } + return null; } - async function updateAnnotations() { + async function updateAnnotations(session, id, data, action) { + // Actually we do not change our dummy data + // We just update the argument in some way and return it + + data.version += 1; + + if (action === 'create') { + let idGenerator = 1000; + data.tracks.concat(data.tags).concat(data.shapes).map((el) => { + el.id = ++idGenerator; + return el; + }); + + return data; + } + + if (action === 'update') { + return data; + } + + if (action === 'delete') { + return data; + } + return null; } @@ -241,7 +275,7 @@ class ServerProxy { frames: { value: Object.freeze({ - getFrame, + getData, getMeta, }), writable: false, -- GitLab