From ed76a3bf2b9e881ede479b68b29b4c3d6918c435 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Wed, 12 Jun 2024 11:54:09 -0400 Subject: [PATCH] Updating authorsmap methods to be more concise, implemented interface to track fields, created more robust error warnings and tested functionality fixing author errors --- examples/timeline-variables.html | 10 +- packages/metadata/src/AuthorsMap.ts | 82 ++++--- packages/metadata/src/index.ts | 35 ++- packages/metadata/tests/metadata-maps.test.ts | 214 ++++++++++++++++++ .../metadata/tests/metadata-module.test.ts | 78 +++++++ 5 files changed, 359 insertions(+), 60 deletions(-) create mode 100644 packages/metadata/tests/metadata-maps.test.ts create mode 100644 packages/metadata/tests/metadata-module.test.ts diff --git a/examples/timeline-variables.html b/examples/timeline-variables.html index e98a462a73..7cc20df8ee 100644 --- a/examples/timeline-variables.html +++ b/examples/timeline-variables.html @@ -20,7 +20,11 @@ }, "Darnell": { givenName: "bob", - } + }, + "Bob": "Bob", + "Ostrich": { + bbq: 10, + }, }, variables: { "trial_type" : { @@ -28,7 +32,7 @@ "chat-plugin": "this chat plugin allows you to talk to gpt!", } }, - "rt" : "adfasfajskf;ajdfddas", + // "rt" : "adfasfajskf;ajdfddas", "time_elapsed": { bbq: "indicates the number of barbeques", description: { @@ -49,7 +53,7 @@ var jsPsych = initJsPsych({ on_finish: async function() { - await metadata.generate(jsPsych.data.get().json()); + await metadata.generate(jsPsych.data.get().json(), metadata_options); // metadata.saveAsJsonFile(); jsPsych.data.displayData(); metadata.displayMetadata(jsPsych.getDisplayElement()); diff --git a/packages/metadata/src/AuthorsMap.ts b/packages/metadata/src/AuthorsMap.ts index 8d0bc9f698..af6d848c2b 100644 --- a/packages/metadata/src/AuthorsMap.ts +++ b/packages/metadata/src/AuthorsMap.ts @@ -1,3 +1,19 @@ +/** + * Interface that defines the type for the fields that are specified for authors + * according to Psych-DS regulations, with name being the one required field. + * + * @export + * @interface AuthorFields + * @typedef {AuthorFields} + */ +export interface AuthorFields { + type?: string; + name: string; + givenName?: string; // required + familyName?: string; + identifier?: string; // identifier that distinguishes across datasets (URL), confusing should check description +} + /** * Class that helps keep track of authors and allows for easy conversion to list format when * generating the final Metadata file. @@ -11,9 +27,9 @@ export class AuthorsMap { * Field that keeps track of the authors in a map. * * @private - * @type {({ [key: string]: {} | string })} + * @type {({ [key: string]: AuthorFields | string })} */ - private authors: { [key: string]: {} | string }; // Define the type for authors + private authors: { [key: string]: AuthorFields | string }; /** * Creates an empty instance of authors map. Doesn't generate default metadata because @@ -28,9 +44,9 @@ export class AuthorsMap { /** * Returns the final list format of the authors according to Psych-DS standards. * - * @returns {({} | string)[]} - List of authors + * @returns {(AuthorFields | string)[]} - List of authors */ - getList(): ({} | string)[] { + getList(): (AuthorFields | string)[] { const author_list = []; for (const key of Object.keys(this.authors)) { author_list.push(this.authors[key]); @@ -42,49 +58,45 @@ export class AuthorsMap { * Method that creates an author. This method can also be used to overwrite existing authors * with the same name in order to update fields. * - * @param {{ - * type?: string; - * name: string; - * givenName?: string; // required - * familyName?: string; - * identifier?: string; // identifier that distinguish across dataset (URL), confusing should check description - * }} fields - All the required or possible fields associated with listing an author according to Psych-DS standards. + * @param {AuthorFields | string} author - All the required or possible fields associated with listing an author according to Psych-DS standards. Option as a string to define an author according only to name. */ - setAuthor(fields: { - type?: string; - name: string; - givenName?: string; // required - familyName?: string; - identifier?: string; // identifier that distinguish across dataset (URL), confusing should check description - }): void { - if (Object.keys(fields).length == 1) { - // if only name, just add to list without dict format, according to documentation - this.authors[fields.name] = fields.name; - return; - } - const new_author: { [key: string]: any } = {}; // Define an empty object to store the variables - new_author["name"] = fields["name"]; // to ensure that name is always first - delete fields["name"]; + setAuthor(author: AuthorFields | string): void { + // handling string + if (typeof author === "string") { + this.authors[author] = author; + } else { + const { type, name, givenName, familyName, identifier, ...rest } = author; - for (const key in fields) { - // Check if the property is defined and not null - if (fields[key] !== undefined && fields[key] !== null) { - new_author[key] = fields[key]; + if (!name) { + console.warn("Name field is missing. Author not added."); + return; } - } - this.authors[new_author.name] = new_author; + const newAuthor: AuthorFields = { name, type, givenName, familyName, identifier, ...rest }; + this.authors[name] = newAuthor; + + if (Object.keys(rest).length > 0) { + // if there are any keys that don't belong + const unexpectedFields = Object.keys(rest).join(", "); + console.warn( + `Unexpected fields (${unexpectedFields}) detected and included in the author object.` + ); + } + } } /** * Method that fetches an author object allowing user to update (in existing workflow should not be necessary). * * @param {string} name - Name of author to be used as key. - * @returns {{}} - Object with author information. + * @returns {(AuthorFields | string | {})} - Object with author information. Empty object if not found. */ - getAuthor(name: string): {} { + getAuthor(name: string): AuthorFields | string | {} { if (name in this.authors) { return this.authors[name]; - } else return {}; + } else { + console.warn("Author (", name, ") not found."); + return {}; + } } } diff --git a/packages/metadata/src/index.ts b/packages/metadata/src/index.ts index 4173f0313e..c8c0637aa8 100644 --- a/packages/metadata/src/index.ts +++ b/packages/metadata/src/index.ts @@ -1,5 +1,4 @@ -import { JsPsych } from "jspsych"; - +import { AuthorFields } from "./AuthorsMap"; import { AuthorsMap } from "./AuthorsMap"; import { VariablesMap } from "./VariablesMap"; @@ -48,7 +47,7 @@ export default class JsPsychMetadata { * @constructor * @param {JsPsych} JsPsych */ - constructor(private JsPsych: JsPsych) { + constructor() { this.generateDefaultMetadata(); } /** @@ -106,31 +105,19 @@ export default class JsPsychMetadata { * Method that creates an author. This method can also be used to overwrite existing authors * with the same name in order to update fields. * - * @param {{ - * type?: string; - * name: string; - * givenName?: string; - * familyName?: string; - * identifier?: string; - * }} fields - All the required or possible fields associated with listing an author according to Psych-DS standards. + * @param {AuthorFields | string} author - All the required or possible fields associated with listing an author according to Psych-DS standards. Option as a string to define an author according only to name. */ - setAuthor(fields: { - type?: string; - name: string; - givenName?: string; // required - familyName?: string; - identifier?: string; // identifier that distinguish across dataset (URL), confusing should check description - }): void { - this.authors.setAuthor(fields); + setAuthor(fields: AuthorFields): void { + this.authors.setAuthor(fields); // Assuming `authors` is an instance of the AuthorsMap class } /** * Method that fetches an author object allowing user to update (in existing workflow should not be necessary). * * @param {string} name - Name of author to be used as key. - * @returns {{}} - Object with author information. + * @returns {(AuthorFields | string | {})} - Object with author information. Empty object if not found. */ - getAuthor(name: string): {} { + getAuthor(name: string): AuthorFields | string | {} { return this.authors.getAuthor(name); } @@ -404,6 +391,7 @@ export default class JsPsychMetadata { } } // iterating through each individual author class else if (key === "author") { + console.log(value); if (typeof value !== "object" || value === null) { console.warn("Author object is not correct type"); return; @@ -411,7 +399,9 @@ export default class JsPsychMetadata { for (const author_key in value) { const author = value[author_key]; - if (!("name" in author)) author["name"] = author_key; + + if (typeof author !== "string" && !("name" in author)) author["name"] = author_key; + this.setAuthor(author); } } else this.setMetadataField(key, value); @@ -457,7 +447,8 @@ export default class JsPsychMetadata { // Return the description return description; } catch (error) { - console.error(`Failed to fetch info from ${unpkgUrl}:`, error); + // console.error(`Failed to fetch info from ${unpkgUrl}:`, error); // DISABLING to test other features + // Error is likely due to 1)a fetch failure, or 2)no JSDoc comments in the script content matched. //HANDLE FETCH FAILURE CASES diff --git a/packages/metadata/tests/metadata-maps.test.ts b/packages/metadata/tests/metadata-maps.test.ts new file mode 100644 index 0000000000..ac54ca67af --- /dev/null +++ b/packages/metadata/tests/metadata-maps.test.ts @@ -0,0 +1,214 @@ +// import { AuthorsMap } from "../src/AuthorsMap"; +// import { VariablesMap } from "../src/VariablesMap"; + +// const author_data = [ +// { +// name: "John Cena", +// identifier: "www.johncena.com", +// }, +// { +// name: "Barrack Obama", +// }, +// { +// type: "Author", +// name: "Donald Trump", +// }, +// { +// type: "Contributor", +// name: "Stan Johnson", +// givenName: "Julio Jones", +// familyName: "Aaron", +// identifier: "www.stantheman", +// }, +// ]; + +// describe("AuthorsMap", () => { +// let authors: AuthorsMap; + +// beforeEach(() => { +// authors = new AuthorsMap(); +// for (const a of author_data) { +// authors.setAuthor(a); +// } +// }); + +// test("#setAndGetAuthor", () => { +// expect(authors.getAuthor(author_data[0]["name"])).toStrictEqual(author_data[0]); +// expect(authors.getAuthor(author_data[1]["name"])).toStrictEqual(author_data[1]["name"]); // when only name, writes string not object according to Psych-DS standards +// expect(authors.getAuthor(author_data[2]["name"])).toStrictEqual(author_data[2]); +// expect(authors.getAuthor(author_data[3]["name"])).toStrictEqual(author_data[3]); +// }); + +// test("#setOverwrite", () => { +// const newJohnCena = { +// type: "WWE Pro Wrestler", +// name: "John Cena", +// }; + +// authors.setAuthor(newJohnCena); +// expect(authors.getAuthor("John Cena")).toStrictEqual(newJohnCena); +// expect(authors.getAuthor("John Cena")).not.toStrictEqual(author_data[0]); +// }); + +// test("#getList", () => { +// const compare = []; + +// for (const a of author_data) { +// compare.push(authors.getAuthor(a["name"])); +// } + +// expect(authors.getList()).toStrictEqual(compare); +// }); +// }); + +// const variable_data = [ +// { +// type: "PropertyValue", +// name: "trial_type", +// description: "Plugin type that has been used to run trials", +// value: "string", +// }, +// { +// type: "PropertyValue", +// name: "trial_index", +// description: "Position of trial in the timeline", +// value: "numeric", +// }, +// { +// type: "PropertyValue", +// name: "time_elapsed", +// description: "Time (in ms) since the start of the experiment", +// value: "numeric", +// }, +// { +// type: "PropertyValue", +// name: "rt (Response time)", +// description: "Time measured in ms participant takes to respond to a stimulus", +// value: "numeric", +// }, +// { +// type: "PropertyValue", +// name: "internal_node_id", +// description: "Internal measurements of node", +// value: "interval", +// }, +// ]; + +// describe("VariablesMap", () => { +// let variablesMap: VariablesMap; + +// beforeEach(() => { +// variablesMap = new VariablesMap(); +// for (const v of variable_data) { +// variablesMap.setVariable(v); +// } +// }); + +// test("#setAndGetVariable", () => { +// expect(variablesMap.getVariable(variable_data[0]["name"])).toStrictEqual(variable_data[0]); +// expect(variablesMap.getVariable(variable_data[1]["name"])).toStrictEqual(variable_data[1]); +// expect(variablesMap.getVariable(variable_data[2]["name"])).toStrictEqual(variable_data[2]); +// expect(variablesMap.getVariable(variable_data[3]["name"])).toStrictEqual(variable_data[3]); +// expect(variablesMap.getVariable(variable_data[4]["name"])).toStrictEqual(variable_data[4]); +// }); + +// test("#setOverwrite", () => { +// const newTrialType = { +// type: "NewPropertyValue", +// name: "trial_type", +// description: "Updated plugin type for running trials", +// value: "boolean", +// }; + +// variablesMap.setVariable(newTrialType); +// expect(variablesMap.getVariable("trial_type")).toStrictEqual(newTrialType); +// expect(variablesMap.getVariable("trial_type")).not.toStrictEqual(variable_data[0]); +// }); + +// test("#getList", () => { +// const compare = []; + +// for (const v of variable_data) { +// compare.push(variablesMap.getVariable(v["name"])); +// } + +// expect(variablesMap.getList()).toStrictEqual(compare); +// }); + +// test("#deleteVariables", () => { +// variablesMap.deleteVariable("trial_type"); +// variablesMap.deleteVariable("trial_index"); +// variablesMap.deleteVariable("internal_node_id"); +// variablesMap.deleteVariable("time_elapsed"); +// variablesMap.deleteVariable("rt (Response time)"); + +// expect(variablesMap.getList().length).toBe(0); +// }); + +// // updating normal variable (exists and doesn't exist) +// test("#updateNormalVariables", () => { +// const compare = { +// type: "PropertyValue", +// name: "trial_type", +// description: "Plugin type that has been used to run trials", +// value: "string", +// }; + +// const new_description = "new description that is super informative!"; +// const new_min_value = 0; +// const new_max_value = 100; + +// variablesMap.updateVariable("trial_type", "description", new_description); +// variablesMap.updateVariable("trial_type", "minValue", new_min_value); +// variablesMap.updateVariable("trial_type", "maxValue", new_max_value); + +// compare["description"] = new_description; +// compare["minValue"] = new_min_value; +// compare["maxValue"] = new_max_value; + +// expect(variablesMap.getVariable("trial_type")).toStrictEqual(compare); +// }); + +// test("#updateLevels", () => { +// const compare = { +// type: "PropertyValue", +// name: "trial_type", +// description: "Plugin type that has been used to run trials", +// value: "string", +// levels: [], +// }; + +// const level1 = "

hello world

"; +// const level2 = "

BOOOOOOO

"; +// const level3 = "

......spot me......

"; + +// variablesMap.updateVariable("trial_type", "levels", level1); +// variablesMap.updateVariable("trial_type", "levels", level2); +// variablesMap.updateVariable("trial_type", "levels", level3); + +// compare["levels"].push(level1); +// compare["levels"].push(level2); +// compare["levels"].push(level3); + +// expect(variablesMap.getVariable("trial_type")).toStrictEqual(compare); +// }); + +// // updating name (checking references) +// test("#updatingName", () => { +// const compare = { +// type: "PropertyValue", +// name: "trial_type", +// description: "Plugin type that has been used to run trials", +// value: "string", +// }; +// const oldName = compare["name"]; +// const newName = "trial_type_updated"; + +// variablesMap.updateVariable("trial_type", "name", newName); +// compare["name"] = newName; + +// expect(variablesMap.getVariable(newName)).toStrictEqual(compare); +// variablesMap.deleteVariable(oldName); +// expect(variablesMap.getVariable(newName)).toStrictEqual(compare); +// }); +// }); diff --git a/packages/metadata/tests/metadata-module.test.ts b/packages/metadata/tests/metadata-module.test.ts new file mode 100644 index 0000000000..675637a62d --- /dev/null +++ b/packages/metadata/tests/metadata-module.test.ts @@ -0,0 +1,78 @@ +import JsPsychMetadata from "../src/index"; + +// missing displaying data modules tests +describe("JsPsychMetadata", () => { + let jsPsychMetadata: JsPsychMetadata; + + beforeEach(() => { + jsPsychMetadata = new JsPsychMetadata(); + jsPsychMetadata.generateDefaultMetadata(); + }); + + test("#setAndGetField", () => { + // Set metadata fields + jsPsychMetadata.setMetadataField("citations", 100); + jsPsychMetadata.setMetadataField("colors", ["green", "yellow", "red"]); + jsPsychMetadata.setMetadataField("description", "Updated description that says nothing"); // update + + // Check if fields are set correctly + expect(jsPsychMetadata.getMetadataField("citations")).toBe(100); + expect(jsPsychMetadata.getMetadataField("colors")).toStrictEqual(["green", "yellow", "red"]); + expect(jsPsychMetadata.getMetadataField("description")).toBe( + "Updated description that says nothing" + ); + + // Check if unset field returns undefined + expect(jsPsychMetadata.getMetadataField("undefinedField")).toBeUndefined(); + }); + + test("#setAndGetAuthor", () => { + const author1 = { + name: "John Cena", + }; + jsPsychMetadata.setAuthor(author1); + expect(jsPsychMetadata.getAuthor("John Cena")).toStrictEqual(author1["name"]); + + author1["type"] = "WWE Pro Wrestler"; + jsPsychMetadata.setAuthor(author1); + expect(jsPsychMetadata.getAuthor("John Cena")).toStrictEqual(author1); + }); + + test("#setAndGetVariable", () => { + const trialType = { + type: "PropertyValue", + name: "trial_type", + description: "Plugin type that has been used to run trials", + value: "string", + }; + + jsPsychMetadata.setVariable(trialType); + expect(jsPsychMetadata.getVariable("trial_type")).toStrictEqual(trialType); + }); + + test("#deleteVariable", () => { + const trialType = { + type: "PropertyValue", + name: "trial_type", + description: "Plugin type that has been used to run trials", + value: "string", + }; + jsPsychMetadata.setVariable(trialType); + + jsPsychMetadata.deleteVariable("trial_type"); + expect(jsPsychMetadata.getVariableNames()).not.toContain("trial_type"); + }); + + test("#updateVariable", () => { + const trialType = { + type: "PropertyValue", + name: "trial_type", + description: "Plugin type that has been used to run trials", + value: "string", + }; + + jsPsychMetadata.updateVariable("trial_type", "levels", 100); + trialType["levels"] = [100]; + expect(jsPsychMetadata.getVariable("trial_type")).toStrictEqual(trialType); + }); +});