Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SurveyJS locale #98

Merged
merged 8 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/khaki-bobcats-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lookit/surveys": patch
---

Set SurveyJS Locale to trial's locale parameter.
14 changes: 1 addition & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions packages/surveys/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
"dependencies": {
"@jspsych/plugin-survey": "^2.0.0",
"dompurify": "^3.0.11",
"marked": "^12.0.1",
"survey-jquery": "^1.9.136"
"marked": "^12.0.1"
},
"devDependencies": {
"@jspsych/config": "^2.0.0",
Expand Down
17 changes: 14 additions & 3 deletions packages/surveys/src/consentSurvey.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import SurveyPlugin from "@jspsych/plugin-survey";
import { TrialType } from "jspsych";
import { ParameterType, TrialType } from "jspsych";
import { consentSurveyFunction } from "./utils";

type Info = typeof SurveyPlugin.info;
const info = <const>{
...SurveyPlugin.info,
parameters: {
...SurveyPlugin.info.parameters,
locale: {
type: ParameterType.STRING,
default: "en-us",
},
},
};

type Info = typeof info;
export type Trial = TrialType<Info>;

/** Consent Survey plugin extends jsPsych's Survey Plugin. */
Expand All @@ -18,7 +29,7 @@ export class ConsentSurveyPlugin extends SurveyPlugin {
public trial(display_element: HTMLElement, trial: Trial) {
super.trial(display_element, {
...trial,
survey_function: consentSurveyFunction(trial.survey_function),
survey_function: consentSurveyFunction(trial),
});
}
/**
Expand Down
7 changes: 7 additions & 0 deletions packages/surveys/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** Error thrown when trial is expecting locale parameter and on is not found. */
export class TrialLocaleParameterUnset extends Error {
/** This will show when the locale is not set in a trial. */
public constructor() {
super("Locale not set in trial parameters.");
}
}
6 changes: 3 additions & 3 deletions packages/surveys/src/exitSurvey.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SurveyPlugin from "@jspsych/plugin-survey";
import chsTemplates from "@lookit/templates";
import { ParameterType, TrialType } from "jspsych";
import { exitSurveyFunction as survey_function } from "./utils";
import { exitSurveyFunction } from "./utils";

const info = <const>{
...SurveyPlugin.info,
Expand Down Expand Up @@ -48,11 +48,11 @@ export class ExitSurveyPlugin extends SurveyPlugin {
* @param display_element - Display element.
* @param trial - Info parameters.
*/
public trial(display_element: HTMLElement, trial: TrialType<Info>) {
public trial(display_element: HTMLElement, trial: Trial) {
super.trial(display_element, {
...trial,
survey_json: chsTemplates.exitSurvey(trial),
survey_function,
survey_function: exitSurveyFunction(trial),
});
}

Expand Down
103 changes: 70 additions & 33 deletions packages/surveys/src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Model } from "survey-jquery";
import { TrialLocaleParameterUnset } from "./errors";
import { Trial } from "./exitSurvey";
import {
consentSurveyFunction,
exitSurveyFunction,
Expand All @@ -10,9 +12,35 @@ jest.mock("@lookit/data", () => ({
updateResponse: jest.fn().mockReturnValue("Response"),
}));

/**
* Helper function to generate a trial object.
*
* @param values - Additonal paramters added to trial object
* @returns Trial object
*/
const getTrial = (values: Record<string, string | boolean | undefined> = {}) =>
({
locale: "en-US",
survey_function: jest.fn(),
...values,
}) as unknown as Trial;

/**
* Helper function to generate surveys for testing.
*
* @param values - Values to add to survey object
* @returns Survey
*/
const getSurvey = (values: Record<string, string | object> = {}) =>
({
onComplete: { add: jest.fn() },
onTextMarkdown: { add: jest.fn() },
...values,
}) as unknown as Model;

test("Markdown to HTML through survey function", () => {
const addMock = jest.fn();
const survey = { onTextMarkdown: { add: addMock } } as unknown as Model;
const survey = getSurvey({ onTextMarkdown: { add: addMock } });
const textValue = "some text";
const options = { text: `**${textValue}**`, html: null };
const rtnSurvey = textMarkdownSurveyFunction(survey);
Expand All @@ -26,11 +54,9 @@ test("Markdown to HTML through survey function", () => {
});

test("Exit survey function", () => {
const survey = {
onComplete: { add: jest.fn() },
onTextMarkdown: { add: jest.fn() },
} as unknown as Model;
const rtnSurvey = exitSurveyFunction(survey);
const survey = getSurvey();
const trial = getTrial();
const rtnSurvey = exitSurveyFunction(trial)(survey);

expect(survey.onComplete.add).toHaveBeenCalledTimes(1);
expect(survey.onTextMarkdown.add).toHaveBeenCalledTimes(1);
Expand All @@ -39,13 +65,11 @@ test("Exit survey function", () => {

test("Anonymous function within exit survey function where withdrawal > 0", () => {
const addMock = jest.fn();
const survey = {
onComplete: { add: addMock },
onTextMarkdown: { add: jest.fn() },
} as unknown as Model;
const survey = getSurvey({ onComplete: { add: addMock } });
const sender = { setValue: jest.fn() };
const trial = getTrial();

exitSurveyFunction(survey);
exitSurveyFunction(trial)(survey);

const anonFn = addMock.mock.calls[0][0];

Expand All @@ -58,13 +82,11 @@ test("Anonymous function within exit survey function where withdrawal > 0", () =

test("Anonymous function within exit survey function where withdrawal is 0", () => {
const addMock = jest.fn();
const survey = {
onComplete: { add: addMock },
onTextMarkdown: { add: jest.fn() },
} as unknown as Model;
const survey = getSurvey({ onComplete: { add: addMock } });
const sender = { setValue: jest.fn() };
const trial = getTrial();

exitSurveyFunction(survey);
exitSurveyFunction(trial)(survey);

const anonFn = addMock.mock.calls[0][0];

Expand All @@ -77,11 +99,9 @@ test("Anonymous function within exit survey function where withdrawal is 0", ()
});

test("Consent survey function", () => {
const survey = {
onComplete: { add: jest.fn() },
onTextMarkdown: { add: jest.fn() },
} as unknown as Model;
const survey_function = consentSurveyFunction();
const survey = getSurvey();
const trial = getTrial();
const survey_function = consentSurveyFunction(trial);
const rtnSurvey = survey_function(survey);

expect(survey.onComplete.add).toHaveBeenCalledTimes(1);
Expand All @@ -90,26 +110,21 @@ test("Consent survey function", () => {
});

test("User function for consent survey function", () => {
const survey = {
onComplete: { add: jest.fn() },
onTextMarkdown: { add: jest.fn() },
} as unknown as Model;
const userFn = jest.fn();
const survey_function = consentSurveyFunction(userFn);
const survey = getSurvey();
const trial = getTrial();
const survey_function = consentSurveyFunction(trial);

survey_function(survey);

expect(userFn).toHaveBeenCalledTimes(1);
expect(trial.survey_function).toHaveBeenCalledTimes(1);
});

test("Anonymous function within consent survey function", () => {
const addMock = jest.fn();
const survey = {
onComplete: { add: addMock },
onTextMarkdown: { add: jest.fn() },
} as unknown as Model;
const survey = getSurvey({ onComplete: { add: addMock } });
const trial = getTrial();

consentSurveyFunction()(survey);
consentSurveyFunction(trial)(survey);

const anonFn = addMock.mock.calls[0][0];

Expand All @@ -120,3 +135,25 @@ test("Anonymous function within consent survey function", () => {
expect(survey.onComplete.add).toHaveBeenCalledTimes(1);
expect(survey.onTextMarkdown.add).toHaveBeenCalledTimes(1);
});

test("Set SurveyJS locale parameter", () => {
const trial = getTrial();
const survey = getSurvey();
exitSurveyFunction(trial)(survey);
expect(survey.locale).toStrictEqual("en-US");
});

test("Set SurveyJS locale parameter to French", () => {
const trial = getTrial({ locale: "fr" });
const survey = getSurvey();
exitSurveyFunction(trial)(survey);
expect(survey.locale).toStrictEqual(trial.locale);
});

test("Survey will throw error if locale is not set", () => {
const trial = getTrial({ locale: undefined });
const survey = getSurvey();
expect(() => exitSurveyFunction(trial)(survey)).toThrow(
TrialLocaleParameterUnset,
);
});
56 changes: 39 additions & 17 deletions packages/surveys/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import Data from "@lookit/data";
import { LookitWindow } from "@lookit/data/dist/types";
import DOMPurify from "dompurify";
import { marked } from "marked";
import { Model } from "survey-jquery";
import { Model } from "survey-core";
import "survey-core/survey.i18n";
import { Trial as ConsentSurveyTrial } from "./consentSurvey";
import { TrialLocaleParameterUnset } from "./errors";
import { Trial as ExitSurveyTrial } from "./exitSurvey";

declare let window: LookitWindow;

type LocaleTrial = ConsentSurveyTrial | ExitSurveyTrial;

const CONFIG = <const>{
marked: { async: false },
dompurify: { USE_PROFILES: { html: true } },
Expand All @@ -28,6 +34,19 @@ export const textMarkdownSurveyFunction = (survey: Model) => {
return survey;
};

/**
* Set locale parameter on SurveyJS Model.
*
* @param survey - SurveyJS model
* @param trial - Trial data including user supplied parameters.
*/
const setSurveyLocale = (survey: Model, trial: LocaleTrial) => {
if (!trial.locale) {
throw new TrialLocaleParameterUnset();
}
survey.locale = new Intl.Locale(trial.locale).baseName;
};

/**
* Survey function used in exit survey. Adds markdown support through
* "textMarkdownSurveyFunction". For the withdrawal checkbox question, this
Expand All @@ -36,31 +55,35 @@ export const textMarkdownSurveyFunction = (survey: Model) => {
* question type rather than boolean with "renderAs: checkbox" because the
* latter doesn't allow both a question title and label next to the checkbox.
*
* @param survey - Survey model provided by SurveyJS.
* @param trial - Trial data including user supplied parameters.
* @returns Survey model.
*/
export const exitSurveyFunction = (survey: Model) => {
textMarkdownSurveyFunction(survey);
export const exitSurveyFunction =
(trial: ExitSurveyTrial) => (survey: Model) => {
setSurveyLocale(survey, trial);
textMarkdownSurveyFunction(survey);

survey.onComplete.add((sender) => {
const trueFalseValue =
sender.getQuestionByName("withdrawal").value.length > 0;
sender.setValue("withdrawal", trueFalseValue);
});
return survey;
};
survey.onComplete.add((sender) => {
const trueFalseValue =
sender.getQuestionByName("withdrawal").value.length > 0;
sender.setValue("withdrawal", trueFalseValue);
});

return survey;
};

/**
* Survey function used by Consent Survey. Adds markdown support through
* "textMarkdownSurveyFunction". On complete, this will mark in the Response
* that consent was completed, and that the consent was through a survey (rather
* than video).
*
* @param userfn - Survey function provided by user.
* @param trial - Trial data including user supplied parameters.
* @returns Survey model.
*/
export const consentSurveyFunction = (userfn?: (x: Model) => Model) => {
return function (survey: Model) {
export const consentSurveyFunction =
(trial: ConsentSurveyTrial) => (survey: Model) => {
setSurveyLocale(survey, trial);
textMarkdownSurveyFunction(survey);

survey.onComplete.add(async () => {
Expand All @@ -70,10 +93,9 @@ export const consentSurveyFunction = (userfn?: (x: Model) => Model) => {
});
});

if (typeof userfn === "function") {
userfn(survey);
if (typeof trial.survey_function === "function") {
trial.survey_function(survey);
}

return survey;
};
};
1 change: 0 additions & 1 deletion packages/templates/src/exitSurveyTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ class ExitSurveyJson {

/** Translate the survey text. */
private translation() {
console.log(this.survey.pages[0].elements[0]);
const { contact_info, name } = window.chs.study.attributes;
const view = {
...this.trial,
Expand Down