From fb04df13e65e7a3bdc326b36d69fdacf349971d0 Mon Sep 17 00:00:00 2001 From: dwithana Date: Mon, 9 Dec 2024 17:51:09 -0500 Subject: [PATCH 1/4] Annotation parser setup, with support multiple motivations --- src/services/annotation-parser.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/annotation-parser.test.js b/src/services/annotation-parser.test.js index adbf004e..85d7315f 100644 --- a/src/services/annotation-parser.test.js +++ b/src/services/annotation-parser.test.js @@ -123,7 +123,7 @@ describe('annotation-parser', () => { const annotations = annotationParser.parseAnnotationSets(textualBodyAnnotations); expect(annotations).toBeNull(); }); - test('returns annotations for AnnotationPage with TextualBody annotations', () => { + test('AnnotationPage with TextualBody annotations', () => { const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(textualBodyAnnotations, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(1); @@ -132,7 +132,7 @@ describe('annotation-parser', () => { expect(label).toEqual('Default'); }); - test('returns annotations for AnnotationPage without TextualBody annotations', () => { + test('AnnotationPage without TextualBody annotations', () => { const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(lunchroomManners, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(1); @@ -142,7 +142,7 @@ describe('annotation-parser', () => { expect(label).toEqual(''); }); - test('returns AnnotationPage info for AnnotationPage without items property', () => { + test('AnnotationPage without items property', () => { const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(externalAnnotationPage, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(4); @@ -159,7 +159,7 @@ describe('annotation-parser', () => { expect(annotationParser.parseAnnotationItems([], 809.0)).toEqual([]); expect(annotationParser.parseAnnotationItems()).toEqual([]); }); - test('parses Annotation with TextualBody type', () => { + test('Annotation with TextualBody type', () => { const annotations = [ { type: 'Annotation', @@ -211,7 +211,7 @@ describe('annotation-parser', () => { { format: 'text/plain', purpose: ['tagging'], value: 'Unknown' }]); }); - test('parses Annotation with Text type', () => { + test('Annotation with Text type', () => { const annotations = [ { id: 'https://example.com/manifest/lunchroom_manners/canvas/1/annotation/1', From ff491a1532867b7ac97481465290448d76f5fdac Mon Sep 17 00:00:00 2001 From: dwithana Date: Wed, 11 Dec 2024 14:54:21 -0500 Subject: [PATCH 2/4] Add unit tests for annotation-parser functions --- src/services/annotation-parser.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/annotation-parser.test.js b/src/services/annotation-parser.test.js index 85d7315f..adbf004e 100644 --- a/src/services/annotation-parser.test.js +++ b/src/services/annotation-parser.test.js @@ -123,7 +123,7 @@ describe('annotation-parser', () => { const annotations = annotationParser.parseAnnotationSets(textualBodyAnnotations); expect(annotations).toBeNull(); }); - test('AnnotationPage with TextualBody annotations', () => { + test('returns annotations for AnnotationPage with TextualBody annotations', () => { const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(textualBodyAnnotations, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(1); @@ -132,7 +132,7 @@ describe('annotation-parser', () => { expect(label).toEqual('Default'); }); - test('AnnotationPage without TextualBody annotations', () => { + test('returns annotations for AnnotationPage without TextualBody annotations', () => { const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(lunchroomManners, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(1); @@ -142,7 +142,7 @@ describe('annotation-parser', () => { expect(label).toEqual(''); }); - test('AnnotationPage without items property', () => { + test('returns AnnotationPage info for AnnotationPage without items property', () => { const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(externalAnnotationPage, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(4); @@ -159,7 +159,7 @@ describe('annotation-parser', () => { expect(annotationParser.parseAnnotationItems([], 809.0)).toEqual([]); expect(annotationParser.parseAnnotationItems()).toEqual([]); }); - test('Annotation with TextualBody type', () => { + test('parses Annotation with TextualBody type', () => { const annotations = [ { type: 'Annotation', @@ -211,7 +211,7 @@ describe('annotation-parser', () => { { format: 'text/plain', purpose: ['tagging'], value: 'Unknown' }]); }); - test('Annotation with Text type', () => { + test('parses Annotation with Text type', () => { const annotations = [ { id: 'https://example.com/manifest/lunchroom_manners/canvas/1/annotation/1', From 51deaad719a838eeeefc213009cccb3502079833 Mon Sep 17 00:00:00 2001 From: dwithana Date: Thu, 12 Dec 2024 17:24:04 -0500 Subject: [PATCH 3/4] [WIP] Parse annotations in linked AnnotationPage resources --- src/services/annotation-parser.test.js | 13 ++- src/services/annotations-parser.js | 124 ++++++++++++++++++------- src/services/utility-helpers.js | 4 +- 3 files changed, 106 insertions(+), 35 deletions(-) diff --git a/src/services/annotation-parser.test.js b/src/services/annotation-parser.test.js index adbf004e..aca134c6 100644 --- a/src/services/annotation-parser.test.js +++ b/src/services/annotation-parser.test.js @@ -118,7 +118,7 @@ const externalAnnotationPage = { }; describe('annotation-parser', () => { - describe('parseAnnotationSets', () => { + describe('parseAnnotationSets()', () => { test('returns null when canvasIndex is undefined', () => { const annotations = annotationParser.parseAnnotationSets(textualBodyAnnotations); expect(annotations).toBeNull(); @@ -154,7 +154,7 @@ describe('annotation-parser', () => { }); }); - describe('parseAnnotationItems', () => { + describe('parseAnnotationItems()', () => { test('returns an empty array for empty list of undefined annotaitons', () => { expect(annotationParser.parseAnnotationItems([], 809.0)).toEqual([]); expect(annotationParser.parseAnnotationItems()).toEqual([]); @@ -328,4 +328,13 @@ describe('annotation-parser', () => { }); }); }); + + describe('parseExternalAnnotationPage', () => { + test('returns empty array for invalid URL', () => { + const annotations = annotationParser.parseExternalAnnotationPage( + 'http://example.com/lunchroom_manners/annotations/caption.vtt', 572.34 + ); + expect(annotations).toEqual([]); + }); + }); }); diff --git a/src/services/annotations-parser.js b/src/services/annotations-parser.js index 633e710f..7b3993b9 100644 --- a/src/services/annotations-parser.js +++ b/src/services/annotations-parser.js @@ -1,5 +1,5 @@ import { getCanvasId } from "./iiif-parser"; -import { getLabelValue, getMediaFragment, parseTimeStrings } from "./utility-helpers"; +import { getLabelValue, getMediaFragment, handleFetchErrors, parseTimeStrings } from "./utility-helpers"; export function parseAnnotationSets(manifest, canvasIndex) { let canvas = null; @@ -17,24 +17,85 @@ export function parseAnnotationSets(manifest, canvasIndex) { const annotations = canvas.annotations; const duration = Number(canvas.duration); - if (annotations?.length > 0 && annotations[0].type === 'AnnotationPage') { - annotations.map((annotation) => { - if (annotation.type === 'AnnotationPage') { - let annotationSet = { label: getLabelValue(annotation.label) }; - if (annotation.items?.length > 0) { - annotationSet.items = parseAnnotationItems(annotation.items, duration); - } else { - annotationSet.url = annotation.id; - } - annotationSets.push(annotationSet); - } - }); - } + annotationSets = parseAnnotationPage(annotations, duration); } return { canvasIndex, annotationSets }; }; +/** + * Fetch and parse linked AnnotationPage json file + * @param {String} url URL of the linked AnnotationPage .json + * @param {Number} duration Canvas duration + * @returns {Object} JSON object for the annotations + * + */ +export async function parseExternalAnnotationPage(url, duration) { + const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]+(\/[\w\-._~:\/?#[\]@!$&'()*+,;=]*)?\.json$/; + + // Validate given URL + if (url == undefined || !url.match(urlRegex)) { + console.log('dasda'); + return []; + } else { + let fileData = null; + + // get file type + await fetch(url) + .then(handleFetchErrors) + .then(function (response) { + fileData = response; + }) + .catch((error) => { + console.error( + 'annotations-parser -> parseExternalAnnotationPage() -> fetching transcript -> ', + error + ); + }); + + if (fileData == null) { + return []; + } else { + try { + const annotationPage = await fileData.json(); + const annotations = parseAnnotationPage([annotationPage], duration); + return annotations; + } catch (e) { + console.error( + 'annotations-parser -> parseExternalAnnotationPage() -> Error: parsing AnnotationPage at, ', + url + ); + return []; + } + } + } +} + +/** + * Parse a annotations in a given list of AnnotationPage objects. + * @param {Array} annotationPages AnnotationPage from either Canvas or linked .json + * @param {Number} duration Canvas duration + * @returns {Array} a parsed list of annotations in the AnnotationPage + * [{ label: String, items: Array }] + */ +function parseAnnotationPage(annotationPages, duration) { + let annotationSets = []; + if (annotationPages?.length > 0 && annotationPages[0].type === 'AnnotationPage') { + annotationPages.map((annotation) => { + if (annotation.type === 'AnnotationPage') { + let annotationSet = { label: getLabelValue(annotation.label) }; + if (annotation.items?.length > 0) { + annotationSet.items = parseAnnotationItems(annotation.items, duration); + } else { + annotationSet.url = annotation.id; + } + annotationSets.push(annotationSet); + } + }); + } + return annotationSets; +} + /** * Parse each Annotation in a given AnnotationPage resource * @param {Array} annotations list of annotations from AnnotationPage @@ -55,12 +116,12 @@ export function parseAnnotationItems(annotations, duration) { let items = []; annotations.map((annotation) => { let canvasId, times; - if (typeof annotation.target === 'string') { + if (typeof annotation?.target === 'string') { canvasId = getCanvasId(annotation.target); times = getMediaFragment(annotation.target, duration); } else { // Might want to re-visit based on the implementation changes in AVAnnotate manifests - const { source, selector } = annotation.target; + const { source, selector } = annotation?.target; canvasId = source.id; times = parseSelector(selector, duration); } @@ -87,7 +148,7 @@ export function parseAnnotationItems(annotations, duration) { */ function parseSelector(selector, duration) { const selectorType = selector.type; - let times; + let times = {}; switch (selectorType) { case 'FragmentSelector': times = parseTimeStrings(selector.value.split('t=')[1], duration); @@ -95,8 +156,6 @@ function parseSelector(selector, duration) { case 'PointSelector': times = { start: Number(selector.t), end: undefined }; break; - default: - break; } return times; }; @@ -108,17 +167,20 @@ function parseSelector(selector, duration) { * { format: String, purpose: Array, value: String } */ function parseTextualBody(textualBody) { - const purpose = textualBody.purpose ? textualBody.purpose : textualBody.motivation; - const annotationBody = { - format: textualBody.format, - /** - * Use purpose instead of motivation, as it is specific to 'TextualBody' type. - * 'purpose'/'motaivation' can have 0 or more values. - * Reference: https://www.w3.org/TR/annotation-model/#motivation-and-purpose - */ - purpose: Array.isArray(purpose) ? purpose : [purpose], - value: textualBody.value, - }; + let annotationBody = {}; + if (textualBody != undefined || textualBody != null) { + const purpose = textualBody.purpose ? textualBody.purpose : textualBody.motivation; + annotationBody = { + format: textualBody.format, + /** + * Use purpose instead of motivation, as it is specific to 'TextualBody' type. + * 'purpose'/'motaivation' can have 0 or more values. + * Reference: https://www.w3.org/TR/annotation-model/#motivation-and-purpose + */ + purpose: Array.isArray(purpose) ? purpose : [purpose], + value: textualBody.value, + }; + } return annotationBody; } @@ -146,8 +208,6 @@ function parseAnnotationBody(annotationBody) { isExternal: true, }); break; - default: - break; } }); return values; diff --git a/src/services/utility-helpers.js b/src/services/utility-helpers.js index a965ad69..a15a4c02 100644 --- a/src/services/utility-helpers.js +++ b/src/services/utility-helpers.js @@ -108,7 +108,9 @@ export function timeToS(time) { * @returns {Object} */ export function handleFetchErrors(response) { - if (!response.ok) { + if (response.status == 404) { + throw new Error('Cannot find the linked resource.'); + } else if (!response.ok) { throw new Error(GENERIC_ERROR_MESSAGE); } return response; From 23d8092e08a55e793bb7f7203136cb872e03cd98 Mon Sep 17 00:00:00 2001 From: dwithana Date: Fri, 13 Dec 2024 13:33:48 -0500 Subject: [PATCH 4/4] Add tests for parsing external AnnotationPage resources --- src/services/annotation-parser.test.js | 402 ++++++++++++++++++++++++- src/services/annotations-parser.js | 11 +- 2 files changed, 396 insertions(+), 17 deletions(-) diff --git a/src/services/annotation-parser.test.js b/src/services/annotation-parser.test.js index aca134c6..2904a1a0 100644 --- a/src/services/annotation-parser.test.js +++ b/src/services/annotation-parser.test.js @@ -1,6 +1,8 @@ import * as annotationParser from './annotations-parser'; import lunchroomManners from '@TestData/lunchroom-manners'; +import emptyManifest from '@TestData/empty-manifest'; +// Manifest with inline TextualBody annotations const textualBodyAnnotations = { '@context': 'http://iiif.io/api/presentation/3/context.json', id: 'https://example.com/avannotate-test/manifest.json', @@ -81,7 +83,8 @@ const textualBodyAnnotations = { ] }; -const externalAnnotationPage = { +// Manifest with linked AnnotationPage resources +const linkedAnnotationPageAnnotations = { '@context': 'http://iiif.io/api/presentation/3/context.json', id: 'https://example.com/avannotate-annotations/manifest.json', type: 'Manifest', @@ -94,29 +97,257 @@ const externalAnnotationPage = { annotations: [ { type: 'AnnotationPage', - id: 'https://example.com/annotations/avannotate-annotations-canvas-1-library-of-congress.json', + id: 'https://example.com/annotations/library-of-congress.json', label: { 'none': ['Library of Congress'] } }, { type: 'AnnotationPage', - id: 'https://example.com/annotations/avannotate-annotations-canvas-1-rolla-southworth.json', + id: 'https://example.com/annotations/rolla-southworth.json', label: { 'none': ['Rolla Southworth'] } }, { type: 'AnnotationPage', - id: 'https://example.com/annotations/avannotate-annotations-canvas-1-zora-neale-hurston.json', + id: 'https://example.com/annotations/zora-neale-hurston.json', label: { 'none': ['Zora Neale Hurston'] } }, { type: 'AnnotationPage', - id: 'https://example.com/annotations/avannotate-annotations-canvas-1-herbert-halpert.json', - label: { 'none': ['Herbert Halpert'] } + id: 'https://example.com/annotations/default.json', + label: { 'none': ['Default'] } }, + ], + items: [ + { + id: "https://example.com/avannotate-annotations/canvas-1/paintings", + type: "AnnotationPage", + items: [ + { + id: "https://example.com/avannotate-annotations/canvas-1/painting", + type: "Annotation", + motivation: "painting", + body: { + id: "https://ia601304.us.archive.org/11/items/WPA_1939_Jacksonville_Halpert/T86-244.mp3", + type: "Sound", + format: "audio/mp3", + duration: 3400 + }, + target: "https://example.com/avannotate-annotations/canvas-1/canvas" + } + ] + } ] } ] }; +// Content in a linked AnnotationPage resource +const annotationPageAnnotations = { + '@context': "http://iiif.io/api/presentation/3/context.json", + id: "https://example.com/annotations/default.json", + type: "AnnotationPage", + label: "Default", + items: [ + { + '@context': "http://www.w3.org/ns/anno.jsonld", + id: "default-annotation-0.json", + type: "Annotation", + motivation: [ + "supplementing", + "commenting" + ], + body: [ + { + type: "TextualBody", + value: "Alabama Singleton. I am 33-years-old.", + format: "text/plain", + purpose: "commenting" + }, + { + type: "TextualBody", + value: "Default", + format: "text/plain", + purpose: "tagging" + } + ], + target: { + source: { + id: "http://example.com/s1576-t86-244/canvas-1/canvas", + type: "Canvas", + partOf: [ + { + id: "http://example.com/s1576-t86-244/manifest.json", + type: "Manifest" + } + ] + }, + selector: { + type: "FragmentSelector", + conformsTo: "http://www.w3.org/TR/media-frags/", + value: "t=2761.474609,2764.772727", + } + } + }, + { + '@context': "http://www.w3.org/ns/anno.jsonld", + id: "default-annotation-1.json", + type: "Annotation", + motivation: [ + "supplementing", + "commenting" + ], + body: [ + { + type: "TextualBody", + value: "Savannah, GA", + format: "text/plain", + purpose: "commenting" + }, + { + type: "TextualBody", + value: "Default", + format: "text/plain", + purpose: "tagging" + } + ], + target: { + source: { + id: "http://example.com/s1576-t86-244/canvas-1/canvas", + type: "Canvas", + partOf: [ + { + id: "http://example.com/s1576-t86-244/manifest.json", + type: "Manifest" + } + ] + }, + selector: { + type: "PointSelector", + t: "2766.438533" + } + } + }, + { + '@context': "http://www.w3.org/ns/anno.jsonld", + id: "default-annotation-2.json", + type: "Annotation", + motivation: [ + "supplementing", + "commenting" + ], + body: [ + { + type: "TextualBody", + value: "A play that we used to play when we were children in Savannah.", + format: "text/plain", + purpose: "commenting" + }, + { + type: "TextualBody", + value: "Default", + format: "text/plain", + purpose: "tagging" + } + ], + target: { + source: { + id: "http://example.com/s1576-t86-244/canvas-1/canvas", + type: "Canvas", + partOf: [ + { + id: "http://example.com/s1576-t86-244/manifest.json", + type: "Manifest" + } + ] + }, + selector: { + type: "FragmentSelector", + conformsTo: "http://www.w3.org/TR/media-frags/", + value: "t=2771.900826,2775.619835", + } + } + }, + { + '@context': "http://www.w3.org/ns/anno.jsonld", + id: "default-annotation-3.json", + type: "Annotation", + motivation: [ + "supplementing", + "commenting" + ], + body: [ + { + type: "TextualBody", + value: "A ring play, just a ring play, a children's ring play", + format: "text/plain", + purpose: "commenting" + }, + { + type: "TextualBody", + value: "Default", + format: "text/plain", + purpose: "tagging" + } + ], + target: { + source: { + id: "http://example.com/s1576-t86-244/canvas-1/canvas", + type: "Canvas", + partOf: [ + { + id: "http://example.com/s1576-t86-244/manifest.json", + type: "Manifest" + } + ] + }, + selector: { + type: "FragmentSelector", + conformsTo: "http://www.w3.org/TR/media-frags/", + value: "t=2779.493802,2782.438017", + } + } + }, + { + '@context': "http://www.w3.org/ns/anno.jsonld", + id: "default-annotation-4.json", + type: "Annotation", + motivation: [ + "supplementing", + "commenting" + ], + body: [ + { + type: "TextualBody", + value: "A ring play, yes. When you say 'go all around the maypole' you'll join hands and be going around the ring and then you're showing your emotion and doing a little dance.", + format: "text/plain", + purpose: "commenting" + }, + { + type: "TextualBody", + value: "Default", + format: "text/plain", + purpose: "tagging" + } + ], + target: { + source: { + id: "http://example.com/s1576-t86-244/canvas-1/canvas", + type: "Canvas", + partOf: [ + { + id: "http://example.com/s1576-t86-244/manifest.json", + type: "Manifest" + } + ] + }, + selector: { + type: "PointSelector", + t: "2835.743802" + } + } + } + ] +}; + describe('annotation-parser', () => { describe('parseAnnotationSets()', () => { test('returns null when canvasIndex is undefined', () => { @@ -143,15 +374,20 @@ describe('annotation-parser', () => { }); test('returns AnnotationPage info for AnnotationPage without items property', () => { - const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(externalAnnotationPage, 0); + const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(linkedAnnotationPageAnnotations, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(4); const { items, label, url } = annotationSets[0]; expect(items).toBeUndefined(); - expect(url).toEqual('https://example.com/annotations/avannotate-annotations-canvas-1-library-of-congress.json'); + expect(url).toEqual('https://example.com/annotations/library-of-congress.json'); expect(label).toEqual('Library of Congress'); }); + + test('returns null for empty Manifest', () => { + const annotations = annotationParser.parseAnnotationSets(emptyManifest, 0); + expect(annotations).toBeNull(); + }); }); describe('parseAnnotationItems()', () => { @@ -159,6 +395,7 @@ describe('annotation-parser', () => { expect(annotationParser.parseAnnotationItems([], 809.0)).toEqual([]); expect(annotationParser.parseAnnotationItems()).toEqual([]); }); + test('parses Annotation with TextualBody type', () => { const annotations = [ { @@ -242,6 +479,50 @@ describe('annotation-parser', () => { }]); }); + describe('parses purpose in the body of Annotation', () => { + test('given as a string', () => { + const annotations = [ + { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: '[Inaudible]', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Inaudible', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=52,60' + } + ]; + const items = annotationParser.parseAnnotationItems(annotations, 809.0); + + expect(items.length).toEqual(1); + expect(items[0].body).toEqual([ + { format: 'text/plain', purpose: ['commenting'], value: '[Inaudible]' }, + { format: 'text/plain', purpose: ['tagging'], value: 'Inaudible' }]); + }); + + test('given as an array', () => { + const annotations = [ + { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: '[Inaudible]', format: 'text/plain', motivation: ['commenting'] }, + { type: 'TextualBody', value: 'Inaudible', format: 'text/plain', motivation: ['tagging'] } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=52,60' + } + ]; + const items = annotationParser.parseAnnotationItems(annotations, 809.0); + + expect(items.length).toEqual(1); + expect(items[0].body).toEqual([ + { format: 'text/plain', purpose: ['commenting'], value: '[Inaudible]' }, + { format: 'text/plain', purpose: ['tagging'], value: 'Inaudible' }]); + }); + }); + describe('parses Annotations with target', () => { test('defined as a string', () => { const annotations = [ @@ -261,6 +542,7 @@ describe('annotation-parser', () => { expect(items[0].times).toEqual({ start: 52, end: 60 }); expect(items[0].canvasId).toEqual('https://example.com/avannotate-test/canvas-1/canvas'); }); + test('defined as a FragmentSelctor', () => { const annotations = [ { @@ -294,6 +576,7 @@ describe('annotation-parser', () => { expect(items[0].times).toEqual({ start: 52, end: 60 }); expect(items[0].canvasId).toEqual('https://example.com/avannotate-test/canvas-1/canvas'); }); + test('defined as a PointSelector', () => { const annotations = [ { @@ -330,11 +613,106 @@ describe('annotation-parser', () => { }); describe('parseExternalAnnotationPage', () => { - test('returns empty array for invalid URL', () => { - const annotations = annotationParser.parseExternalAnnotationPage( + describe('parses annotations for valid linked AnnotationPage', () => { + let fetchSpy, annotations; + beforeEach(async () => { + fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce({ + status: 201, + ok: true, + json: jest.fn(() => { return annotationPageAnnotations; }) + }); + + annotations = await annotationParser.parseExternalAnnotationPage( + 'https://example.com/annotations/default.json', 3400 + ); + }); + + test('returns a list Annotation from the AnnotationPage', () => { + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(fetchSpy).toHaveBeenCalledWith('https://example.com/annotations/default.json'); + expect(annotations.length).toEqual(1); + + const { label, items } = annotations[0]; + expect(label).toEqual('Default'); + expect(items).toHaveLength(5); + }); + + test('returns range Annotation', () => { + const { _, items } = annotations[0]; + expect(items[0]).toEqual({ + motivation: ['supplementing', 'commenting'], + id: 'default-annotation-0.json', + times: { start: 2761.474609, end: 2764.772727 }, + canvasId: 'http://example.com/s1576-t86-244/canvas-1/canvas', + body: [ + { format: 'text/plain', purpose: ['commenting'], value: 'Alabama Singleton. I am 33-years-old.' }, + { format: 'text/plain', purpose: ['tagging'], value: 'Default' } + ] + }); + }); + + test('returns time-point Annotation', () => { + const { _, items } = annotations[0]; + expect(items[1]).toEqual({ + motivation: ['supplementing', 'commenting'], + id: 'default-annotation-1.json', + times: { start: 2766.438533, end: undefined }, + canvasId: 'http://example.com/s1576-t86-244/canvas-1/canvas', + body: [ + { format: 'text/plain', purpose: ['commenting'], value: 'Savannah, GA' }, + { format: 'text/plain', purpose: ['tagging'], value: 'Default' } + ] + }); + }); + + }); + + test('returns an empty array for invalid AnnotationPage link', async () => { + expect(await annotationParser.parseExternalAnnotationPage( 'http://example.com/lunchroom_manners/annotations/caption.vtt', 572.34 - ); - expect(annotations).toEqual([]); + )).toEqual([]); + + expect(await annotationParser.parseExternalAnnotationPage()).toEqual([]); + }); + + test('returns an empty array for failed fetch request', async () => { + // Mock console.error function + const originalError = console.error; + console.error = jest.fn(); + + const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce({ + status: 404, + }); + + expect(await annotationParser.parseExternalAnnotationPage( + 'http://example.com/lunchroom_manners/annotations/default.json', 572.34 + )).toEqual([]); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledTimes(1); + + // Cleanup: restore console.error + console.error = originalError; + }); + + test('returns an empty array for invalid response body', async () => { + // Mock console.error function + const originalError = console.error; + console.error = jest.fn(); + + const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce({ + status: 201, + ok: true, + json: jest.fn(() => { return Promise.reject(new Error()); }) + }); + + expect(await annotationParser.parseExternalAnnotationPage( + 'http://example.com/lunchroom_manners/annotations/default.json', 572.34 + )).toEqual([]); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledTimes(1); + + // Cleanup: restore console.error + console.error = originalError; }); }); }); diff --git a/src/services/annotations-parser.js b/src/services/annotations-parser.js index 7b3993b9..eca4c9a7 100644 --- a/src/services/annotations-parser.js +++ b/src/services/annotations-parser.js @@ -18,9 +18,10 @@ export function parseAnnotationSets(manifest, canvasIndex) { const duration = Number(canvas.duration); annotationSets = parseAnnotationPage(annotations, duration); + return { canvasIndex, annotationSets }; + } else { + return null; } - - return { canvasIndex, annotationSets }; }; /** @@ -34,8 +35,7 @@ export async function parseExternalAnnotationPage(url, duration) { const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]+(\/[\w\-._~:\/?#[\]@!$&'()*+,;=]*)?\.json$/; // Validate given URL - if (url == undefined || !url.match(urlRegex)) { - console.log('dasda'); + if (url == undefined || url.match(urlRegex) == null) { return []; } else { let fileData = null; @@ -51,6 +51,7 @@ export async function parseExternalAnnotationPage(url, duration) { 'annotations-parser -> parseExternalAnnotationPage() -> fetching transcript -> ', error ); + return []; }); if (fileData == null) { @@ -168,7 +169,7 @@ function parseSelector(selector, duration) { */ function parseTextualBody(textualBody) { let annotationBody = {}; - if (textualBody != undefined || textualBody != null) { + if (textualBody) { const purpose = textualBody.purpose ? textualBody.purpose : textualBody.motivation; annotationBody = { format: textualBody.format,