jspsych 7.3.4 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/css/jspsych.css +18 -8
  3. package/dist/index.browser.js +3080 -4286
  4. package/dist/index.browser.js.map +1 -1
  5. package/dist/index.browser.min.js +6 -2
  6. package/dist/index.browser.min.js.map +1 -1
  7. package/dist/index.cjs +2314 -4066
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +984 -6
  10. package/dist/index.js +2313 -4066
  11. package/dist/index.js.map +1 -1
  12. package/package.json +6 -5
  13. package/src/ExtensionManager.spec.ts +123 -0
  14. package/src/ExtensionManager.ts +81 -0
  15. package/src/JsPsych.ts +195 -690
  16. package/src/ProgressBar.spec.ts +60 -0
  17. package/src/ProgressBar.ts +60 -0
  18. package/src/index.scss +29 -8
  19. package/src/index.ts +4 -9
  20. package/src/modules/data/DataCollection.ts +1 -1
  21. package/src/modules/data/DataColumn.ts +12 -1
  22. package/src/modules/data/index.ts +92 -103
  23. package/src/modules/extensions.ts +4 -0
  24. package/src/modules/plugin-api/AudioPlayer.ts +101 -0
  25. package/src/modules/plugin-api/KeyboardListenerAPI.ts +1 -1
  26. package/src/modules/plugin-api/MediaAPI.ts +48 -106
  27. package/src/modules/plugin-api/__mocks__/AudioPlayer.ts +38 -0
  28. package/src/modules/plugin-api/index.ts +11 -14
  29. package/src/modules/plugins.ts +26 -27
  30. package/src/modules/randomization.ts +1 -1
  31. package/src/timeline/Timeline.spec.ts +921 -0
  32. package/src/timeline/Timeline.ts +342 -0
  33. package/src/timeline/TimelineNode.ts +174 -0
  34. package/src/timeline/Trial.spec.ts +897 -0
  35. package/src/timeline/Trial.ts +419 -0
  36. package/src/timeline/index.ts +232 -0
  37. package/src/timeline/util.spec.ts +124 -0
  38. package/src/timeline/util.ts +146 -0
  39. package/dist/JsPsych.d.ts +0 -112
  40. package/dist/TimelineNode.d.ts +0 -34
  41. package/dist/migration.d.ts +0 -3
  42. package/dist/modules/data/DataCollection.d.ts +0 -46
  43. package/dist/modules/data/DataColumn.d.ts +0 -15
  44. package/dist/modules/data/index.d.ts +0 -25
  45. package/dist/modules/data/utils.d.ts +0 -3
  46. package/dist/modules/extensions.d.ts +0 -22
  47. package/dist/modules/plugin-api/HardwareAPI.d.ts +0 -15
  48. package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +0 -34
  49. package/dist/modules/plugin-api/MediaAPI.d.ts +0 -32
  50. package/dist/modules/plugin-api/SimulationAPI.d.ts +0 -44
  51. package/dist/modules/plugin-api/TimeoutAPI.d.ts +0 -17
  52. package/dist/modules/plugin-api/index.d.ts +0 -8
  53. package/dist/modules/plugins.d.ts +0 -136
  54. package/dist/modules/randomization.d.ts +0 -42
  55. package/dist/modules/turk.d.ts +0 -40
  56. package/dist/modules/utils.d.ts +0 -13
  57. package/src/TimelineNode.ts +0 -544
  58. package/src/modules/plugin-api/HardwareAPI.ts +0 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jspsych",
3
- "version": "7.3.4",
3
+ "version": "8.0.0",
4
4
  "description": "Behavioral experiments in a browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -43,21 +43,22 @@
43
43
  "dependencies": {
44
44
  "auto-bind": "^4.0.0",
45
45
  "random-words": "^1.1.1",
46
- "seedrandom": "^3.0.5"
46
+ "seedrandom": "^3.0.5",
47
+ "type-fest": "^2.9.0"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@fontsource/open-sans": "4.5.3",
50
- "@jspsych/config": "^2.0.0",
51
+ "@jspsych/config": "^3.0.0",
51
52
  "@types/dom-mediacapture-record": "^1.0.11",
52
53
  "base64-inline-loader": "^2.0.1",
53
54
  "css-loader": "^6.6.0",
54
55
  "mini-css-extract-plugin": "^2.5.3",
55
56
  "npm-run-all": "^4.1.5",
57
+ "replace-in-file-webpack-plugin": "^1.0.6",
56
58
  "sass": "^1.43.5",
57
59
  "sass-loader": "^12.4.0",
58
60
  "webpack": "^5.76.0",
59
61
  "webpack-cli": "^4.9.2",
60
- "webpack-remove-empty-scripts": "^0.7.2",
61
- "replace-in-file-webpack-plugin": "^1.0.6"
62
+ "webpack-remove-empty-scripts": "^0.7.2"
62
63
  }
63
64
  }
@@ -0,0 +1,123 @@
1
+ import { Class } from "type-fest";
2
+
3
+ import { TestExtension } from "../tests/extensions/test-extension";
4
+ import { ExtensionManager, ExtensionManagerDependencies } from "./ExtensionManager";
5
+ import { JsPsych } from "./JsPsych";
6
+ import { JsPsychExtension } from "./modules/extensions";
7
+
8
+ jest.mock("../src/JsPsych");
9
+
10
+ export class ExtensionManagerDependenciesMock implements ExtensionManagerDependencies {
11
+ instantiateExtension: jest.Mock<JsPsychExtension>;
12
+
13
+ jsPsych: JsPsych; // to be passed to extensions by `instantiateExtension`
14
+
15
+ constructor() {
16
+ this.initializeProperties();
17
+ }
18
+
19
+ private initializeProperties() {
20
+ this.instantiateExtension = jest.fn(
21
+ (extensionClass: Class<JsPsychExtension>) => new extensionClass(this.jsPsych)
22
+ );
23
+
24
+ this.jsPsych = new JsPsych();
25
+ }
26
+
27
+ reset() {
28
+ this.initializeProperties();
29
+ }
30
+ }
31
+
32
+ const dependencies = new ExtensionManagerDependenciesMock();
33
+ afterEach(() => {
34
+ dependencies.reset();
35
+ });
36
+
37
+ describe("ExtensionManager", () => {
38
+ it("instantiates all extensions upon construction", () => {
39
+ new ExtensionManager(dependencies, [{ type: TestExtension }]);
40
+
41
+ expect(dependencies.instantiateExtension).toHaveBeenCalledTimes(1);
42
+ expect(dependencies.instantiateExtension).toHaveBeenCalledWith(TestExtension);
43
+ });
44
+
45
+ it("exposes extensions via the `extensions` property", () => {
46
+ const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]);
47
+
48
+ expect(manager.extensions).toEqual({ test: expect.any(TestExtension) });
49
+ });
50
+
51
+ describe("initialize()", () => {
52
+ it("calls `initialize` on all extensions, providing the parameters from the constructor", async () => {
53
+ const manager = new ExtensionManager(dependencies, [
54
+ { type: TestExtension, params: { option: 1 } },
55
+ ]);
56
+
57
+ await manager.initializeExtensions();
58
+
59
+ expect(manager.extensions.test.initialize).toHaveBeenCalledTimes(1);
60
+ expect(manager.extensions.test.initialize).toHaveBeenCalledWith({ option: 1 });
61
+ });
62
+
63
+ it("defaults `params` to an empty object", async () => {
64
+ const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]);
65
+
66
+ await manager.initializeExtensions();
67
+ expect(manager.extensions.test.initialize).toHaveBeenCalledWith({});
68
+ });
69
+ });
70
+
71
+ describe("onStart()", () => {
72
+ it("calls `on_start` on all extensions specified in the provided `extensions` parameter", () => {
73
+ const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]);
74
+
75
+ const onStartCallback = jest.mocked(manager.extensions.test.on_start);
76
+
77
+ manager.onStart();
78
+ expect(onStartCallback).not.toHaveBeenCalled();
79
+
80
+ manager.onStart([{ type: TestExtension, params: { my: "option" } }]);
81
+ expect(onStartCallback).toHaveBeenCalledWith({ my: "option" });
82
+ });
83
+ });
84
+
85
+ describe("onLoad()", () => {
86
+ it("calls `on_load` on all extensions specified in the provided `extensions` parameter", () => {
87
+ const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]);
88
+
89
+ const onLoadCallback = jest.mocked(manager.extensions.test.on_load);
90
+
91
+ manager.onLoad();
92
+ expect(onLoadCallback).not.toHaveBeenCalled();
93
+
94
+ manager.onLoad([{ type: TestExtension, params: { my: "option" } }]);
95
+ expect(onLoadCallback).toHaveBeenCalledWith({ my: "option" });
96
+ });
97
+ });
98
+
99
+ describe("onFinish()", () => {
100
+ it("calls `on_finish` on all extensions specified in the provided `extensions` parameter and returns a joint extension results object", async () => {
101
+ const manager = new ExtensionManager(dependencies, [{ type: TestExtension }]);
102
+
103
+ const onFinishCallback = jest.mocked(manager.extensions.test.on_finish);
104
+ onFinishCallback.mockReturnValue({
105
+ extension_type: "test",
106
+ extension_version: "1.0",
107
+ extension: "result",
108
+ });
109
+
110
+ let results = await manager.onFinish(undefined);
111
+ expect(onFinishCallback).not.toHaveBeenCalled();
112
+ expect(results).toEqual({});
113
+
114
+ results = await manager.onFinish([{ type: TestExtension, params: { my: "option" } }]);
115
+ expect(onFinishCallback).toHaveBeenCalledWith({ my: "option" });
116
+ expect(results).toEqual({
117
+ extension_type: ["test"],
118
+ extension_version: ["1.0"],
119
+ extension: "result",
120
+ });
121
+ });
122
+ });
123
+ });
@@ -0,0 +1,81 @@
1
+ import { Class } from "type-fest";
2
+
3
+ import { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions";
4
+ import { TrialExtensionsConfiguration } from "./timeline";
5
+
6
+ export type GlobalExtensionsConfiguration = Array<{
7
+ type: Class<JsPsychExtension>;
8
+ params?: Record<string, any>;
9
+ }>;
10
+
11
+ export interface ExtensionManagerDependencies {
12
+ /**
13
+ * Given an extension class, create a new instance of it and return it.
14
+ */
15
+ instantiateExtension(extensionClass: Class<JsPsychExtension>): JsPsychExtension;
16
+ }
17
+
18
+ export class ExtensionManager {
19
+ private static getExtensionNameByClass(extensionClass: Class<JsPsychExtension>) {
20
+ return (extensionClass["info"] as JsPsychExtensionInfo).name;
21
+ }
22
+
23
+ public readonly extensions: Record<string, JsPsychExtension>;
24
+
25
+ constructor(
26
+ private dependencies: ExtensionManagerDependencies,
27
+ private extensionsConfiguration: GlobalExtensionsConfiguration
28
+ ) {
29
+ this.extensions = Object.fromEntries(
30
+ extensionsConfiguration.map((extension) => [
31
+ ExtensionManager.getExtensionNameByClass(extension.type),
32
+ this.dependencies.instantiateExtension(extension.type),
33
+ ])
34
+ );
35
+ }
36
+
37
+ private getExtensionInstanceByClass(extensionClass: Class<JsPsychExtension>) {
38
+ return this.extensions[ExtensionManager.getExtensionNameByClass(extensionClass)];
39
+ }
40
+
41
+ public async initializeExtensions() {
42
+ await Promise.all(
43
+ this.extensionsConfiguration.map(({ type, params = {} }) =>
44
+ this.getExtensionInstanceByClass(type).initialize(params)
45
+ )
46
+ );
47
+ }
48
+
49
+ public onStart(trialExtensionsConfiguration: TrialExtensionsConfiguration = []) {
50
+ for (const { type, params } of trialExtensionsConfiguration) {
51
+ this.getExtensionInstanceByClass(type)?.on_start(params);
52
+ }
53
+ }
54
+
55
+ public onLoad(trialExtensionsConfiguration: TrialExtensionsConfiguration = []) {
56
+ for (const { type, params } of trialExtensionsConfiguration) {
57
+ this.getExtensionInstanceByClass(type)?.on_load(params);
58
+ }
59
+ }
60
+
61
+ public async onFinish(
62
+ trialExtensionsConfiguration: TrialExtensionsConfiguration = []
63
+ ): Promise<Record<string, any>> {
64
+ const results = await Promise.all(
65
+ trialExtensionsConfiguration.map(({ type, params }) =>
66
+ Promise.resolve(this.getExtensionInstanceByClass(type)?.on_finish(params))
67
+ )
68
+ );
69
+
70
+ const extensionInfo = trialExtensionsConfiguration.length
71
+ ? {
72
+ extension_type: results.map((result) => result.extension_type),
73
+ extension_version: results.map((result) => result.extension_version),
74
+ }
75
+ : {};
76
+
77
+ results.push(extensionInfo);
78
+
79
+ return Object.assign({}, ...results);
80
+ }
81
+ }