Upgrade Jest to v29 and replace ts-jest with @sucrase/jest-plugin

This commit is contained in:
bjoluc 2022-11-17 21:28:44 +01:00
parent 0bc6f5486b
commit 76e7508024
16 changed files with 2736 additions and 17244 deletions

View File

@ -0,0 +1,5 @@
---
"@jspsych/config": major
---
Activate TypeScript's `isolatedModules` flag in the root `tsconfig.json` file. If you are facing any TypeScript errors due to `isolatedModules`, please update your code according to the error messages.

View File

@ -32,3 +32,4 @@ Rewrite jsPsych's core logic. The following breaking changes have been made:
- Interaction listeners are now removed when the experiment ends. - Interaction listeners are now removed when the experiment ends.
- JsPsych will now throw an error when a non-array value is used for a trial parameter marked as `array: true` in the plugin's info object. - JsPsych will now throw an error when a non-array value is used for a trial parameter marked as `array: true` in the plugin's info object.
- JsPsych now internally relies on the JavaScript event loop. This means automated tests have to `await` utility functions like `pressKey()` to process the event loop. - JsPsych now internally relies on the JavaScript event loop. This means automated tests have to `await` utility functions like `pressKey()` to process the event loop.
- The `jspsych` package no longer exports `universalPluginParameters` and the `UniversalPluginParameters` type.

View File

@ -0,0 +1,5 @@
---
"@jspsych/config": major
---
Upgrade Jest to v29 and replace ts-jest with the more performant Sucrase Jest plugin. As a consequence, Jest does no longer type-check code. Please check Jest's [upgrade guide](https://jestjs.io/docs/upgrading-to-jest29) for instructions on updating your tests.

View File

@ -35,6 +35,9 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node-${{ matrix.node }}-turbo- ${{ runner.os }}-node-${{ matrix.node }}-turbo-
- name: Check types
run: npm run tsc
- name: Build packages - name: Build packages
run: npm run build run: npm run build
@ -44,8 +47,7 @@ jobs:
- name: Run tests - name: Run tests
run: npm run test -- --ci --coverage --maxWorkers=2 --reporters=default --reporters=github-actions run: npm run test -- --ci --coverage --maxWorkers=2 --reporters=default --reporters=github-actions
env:
NODE_OPTIONS: "--max-old-space-size=4096" # Increase heap size for jest
# TODO setup codecov or coveralls # TODO setup codecov or coveralls
# - name: Upload coverage to Codecov # - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v1 # uses: codecov/codecov-action@v1

19797
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,14 +27,13 @@
"devDependencies": { "devDependencies": {
"@changesets/changelog-github": "^0.4.7", "@changesets/changelog-github": "^0.4.7",
"@changesets/cli": "^2.25.2", "@changesets/cli": "^2.25.2",
"@jspsych/config": "^1.3.2",
"husky": "^8.0.2", "husky": "^8.0.2",
"import-sort-style-module": "^6.0.0", "import-sort-style-module": "^6.0.0",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-plugin-import-sort": "^0.0.7", "prettier-plugin-import-sort": "^0.0.7",
"turbo": "^1.6.3", "turbo": "^1.6.3"
"jest": "*",
"ts-jest": "*"
}, },
"prettier": { "prettier": {
"printWidth": 100 "printWidth": 100

View File

@ -1,21 +1,14 @@
const ts = require("typescript"); const hq = require("alias-hq");
const { pathsToModuleNameMapper } = require("ts-jest");
/** @type { (dirname: string) => import('@jest/types').Config.InitialOptions } */ /** @type { (dirname: string) => import('@jest/types').Config.InitialOptions } */
module.exports.makePackageConfig = (dirname) => { module.exports.makePackageConfig = (dirname) => {
const packageJson = require(dirname + "/package.json"); const packageJson = require(dirname + "/package.json");
const packageBaseName = packageJson.name.replace("@jspsych/", ""); const packageBaseName = packageJson.name.replace("@jspsych/", "");
// based on https://github.com/formium/tsdx/blob/462af2d002987f985695b98400e0344b8f2754b7/src/createRollupConfig.ts#L51-L57
const tsCompilerOptions = ts.parseJsonConfigFileContent(
ts.readConfigFile(dirname + "/tsconfig.json", ts.sys.readFile).config,
ts.sys,
dirname
).options;
return { return {
preset: "ts-jest", transform: { "\\.(js|jsx|ts|tsx)$": "@sucrase/jest-plugin" },
moduleNameMapper: pathsToModuleNameMapper(tsCompilerOptions.paths, { prefix: "<rootDir>/" }), moduleNameMapper: hq.load(dirname + "/tsconfig.json").get("jest"),
testEnvironment: "jsdom", testEnvironment: "jsdom",
testEnvironmentOptions: { testEnvironmentOptions: {
fetchExternalResources: true, fetchExternalResources: true,

View File

@ -47,25 +47,27 @@
"@rollup/plugin-json": "4.1.0", "@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "13.3.0", "@rollup/plugin-node-resolve": "13.3.0",
"@rollup/plugin-replace": "4.0.0", "@rollup/plugin-replace": "4.0.0",
"@sucrase/jest-plugin": "3.0.0",
"@types/gulp": "4.0.9", "@types/gulp": "4.0.9",
"@types/jest": "27.5.1", "@types/jest": "29.2.3",
"alias-hq": "github:bjoluc/alias-hq#fix-jest-plugin",
"babel-preset-minify": "0.5.2", "babel-preset-minify": "0.5.2",
"canvas": "2.9.1", "canvas": "2.9.1",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-cli": "2.3.0", "gulp-cli": "2.3.0",
"gulp-file": "^0.4.0", "gulp-file": "0.4.0",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-replace": "1.1.3", "gulp-replace": "1.1.3",
"gulp-zip": "5.1.0", "gulp-zip": "5.1.0",
"jest": "28.1.0", "jest": "29.3.1",
"jest-environment-jsdom": "28.1.0", "jest-environment-jsdom": "29.3.1",
"merge-stream": "2.0.0", "merge-stream": "2.0.0",
"regenerator-runtime": "0.13.9", "regenerator-runtime": "0.13.9",
"rollup": "2.73.0", "rollup": "2.73.0",
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-typescript2": "0.31.2", "rollup-plugin-typescript2": "0.31.2",
"ts-jest": "28.0.2", "sucrase": "3.29.0",
"tslib": "2.4.0", "tslib": "2.4.0",
"typescript": "^4.6.4" "typescript": "4.6.4"
} }
} }

View File

@ -1,33 +1,21 @@
{ {
// shared base tsconfig for all jsPsych packages
// based on https://github.com/formium/tsdx/blob/462af2d002987f985695b98400e0344b8f2754b7/templates/basic/tsconfig.json
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
"compilerOptions": { "compilerOptions": {
"target": "ES6", "target": "ES6",
"module": "ESNext", "module": "ESNext",
"lib": ["dom", "esnext"], "lib": ["dom", "esnext"],
"importHelpers": true, "importHelpers": true,
// output .d.ts declaration files for consumers
"declaration": true, "declaration": true,
// output .js.map sourcemap files for consumers
"sourceMap": true, "sourceMap": true,
// stricter type-checking for stronger correctness. Recommended by TS
"strict": false, // should be enabled one lucky day "strict": false, // should be enabled one lucky day
// linter checks for common issues
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedLocals": false, // should be enabled one lucky day "noUnusedLocals": false, // should be enabled one lucky day
"noUnusedParameters": false, // should be enabled one lucky day "noUnusedParameters": false, // should be enabled one lucky day
// use Node's module resolution algorithm, instead of the legacy TS one
"moduleResolution": "node", "moduleResolution": "node",
// interop between ESM and CJS modules. Recommended by TS
"esModuleInterop": true, "esModuleInterop": true,
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
"skipLibCheck": true, "skipLibCheck": true,
// error out if import and file system have a casing mismatch. Recommended by TS
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
// do not emit build output when running `tsc` "noEmit": true,
"noEmit": true "isolatedModules": true // required by Sucrase
} }
} }

View File

@ -11,7 +11,7 @@ describe("ProgressBar", () => {
it("sets up proper HTML markup when created", () => { it("sets up proper HTML markup when created", () => {
expect(containerElement.innerHTML).toMatchInlineSnapshot( expect(containerElement.innerHTML).toMatchInlineSnapshot(
'"<span>My message</span><div id=\\"jspsych-progressbar-outer\\"><div id=\\"jspsych-progressbar-inner\\" style=\\"width: 0%;\\"></div></div>"' '"<span>My message</span><div id="jspsych-progressbar-outer"><div id="jspsych-progressbar-inner" style="width: 0%;"></div></div>"'
); );
}); });

View File

@ -62,12 +62,6 @@ export function initJsPsych(options?) {
} }
export { JsPsych } from "./JsPsych"; export { JsPsych } from "./JsPsych";
export { export type { JsPsychPlugin, PluginInfo, TrialType } from "./modules/plugins";
JsPsychPlugin, export { ParameterType } from "./modules/plugins";
PluginInfo, export type { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions";
TrialType,
ParameterType,
universalPluginParameters,
UniversalPluginParameters,
} from "./modules/plugins";
export { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions";

View File

@ -295,9 +295,9 @@ describe("Timeline", () => {
describe("with timeline variables", () => { describe("with timeline variables", () => {
it("repeats all trials for each set of variables", async () => { it("repeats all trials for each set of variables", async () => {
const xValues = []; const xValues = [];
TestPlugin.prototype.trial.mockImplementation(async () => { TestPlugin.trial = async () => {
xValues.push(timeline.evaluateTimelineVariable(new TimelineVariable("x"))); xValues.push(timeline.evaluateTimelineVariable(new TimelineVariable("x")));
}); };
const timeline = createTimeline({ const timeline = createTimeline({
timeline: [{ type: TestPlugin }], timeline: [{ type: TestPlugin }],
@ -320,9 +320,9 @@ describe("Timeline", () => {
sample, sample,
randomize_order, randomize_order,
}); });
TestPlugin.prototype.trial.mockImplementation(async () => { TestPlugin.trial = async () => {
xValues.push(timeline.evaluateTimelineVariable(new TimelineVariable("x"))); xValues.push(timeline.evaluateTimelineVariable(new TimelineVariable("x")));
}); };
return timeline; return timeline;
}; };

View File

@ -106,13 +106,11 @@ describe("Trial", () => {
await trial1.run(); await trial1.run();
expect(trial1.getResult()).toEqual(expect.objectContaining({ my: "result" })); expect(trial1.getResult()).toEqual(expect.objectContaining({ my: "result" }));
jest TestPlugin.trial = async (display_element, trial, on_load) => {
.spyOn(TestPlugin.prototype, "trial")
.mockImplementation(async (display_element, trial, on_load) => {
on_load(); on_load();
dependencies.finishTrialPromise.resolve({ finishTrial: "result" }); dependencies.finishTrialPromise.resolve({ finishTrial: "result" });
return { my: "result" }; return { my: "result" };
}); };
const trial2 = createTrial({ type: TestPlugin }); const trial2 = createTrial({ type: TestPlugin });
await trial2.run(); await trial2.run();
@ -122,9 +120,9 @@ describe("Trial", () => {
describe("if `trial` returns no promise", () => { describe("if `trial` returns no promise", () => {
beforeAll(() => { beforeAll(() => {
TestPlugin.prototype.trial.mockImplementation(() => { TestPlugin.trial = () => {
dependencies.finishTrialPromise.resolve({ my: "result" }); dependencies.finishTrialPromise.resolve({ my: "result" });
}); };
}); });
it("invokes the local `on_load` callback", async () => { it("invokes the local `on_load` callback", async () => {
@ -216,11 +214,11 @@ describe("Trial", () => {
complexArrayParameter: { type: ParameterType.COMPLEX, array: true }, complexArrayParameter: { type: ParameterType.COMPLEX, array: true },
functionParameter: { type: ParameterType.FUNCTION }, functionParameter: { type: ParameterType.FUNCTION },
}); });
TestPlugin.setDefaultTrialResult({ TestPlugin.defaultTrialResult = {
result: "foo", result: "foo",
stringParameter2: "string", stringParameter2: "string",
stringParameter3: "string", stringParameter3: "string",
}); };
const trial = createTrial({ const trial = createTrial({
type: TestPlugin, type: TestPlugin,
stringParameter1: "string", stringParameter1: "string",
@ -367,12 +365,12 @@ describe("Trial", () => {
await expect( await expect(
createTrial({ type: TestPlugin, stringArray: {} }).run() createTrial({ type: TestPlugin, stringArray: {} }).run()
).rejects.toThrowErrorMatchingInlineSnapshot( ).rejects.toThrowErrorMatchingInlineSnapshot(
'"A non-array value (`[object Object]`) was provided for the array parameter \\"stringArray\\" in the \\"test\\" plugin. Please make sure that \\"stringArray\\" is an array."' '"A non-array value (`[object Object]`) was provided for the array parameter "stringArray" in the "test" plugin. Please make sure that "stringArray" is an array."'
); );
await expect( await expect(
createTrial({ type: TestPlugin, stringArray: 1 }).run() createTrial({ type: TestPlugin, stringArray: 1 }).run()
).rejects.toThrowErrorMatchingInlineSnapshot( ).rejects.toThrowErrorMatchingInlineSnapshot(
'"A non-array value (`1`) was provided for the array parameter \\"stringArray\\" in the \\"test\\" plugin. Please make sure that \\"stringArray\\" is an array."' '"A non-array value (`1`) was provided for the array parameter "stringArray" in the "test" plugin. Please make sure that "stringArray" is an array."'
); );
}); });

View File

@ -21,11 +21,7 @@ class TestPlugin implements JsPsychPlugin<typeof testPluginInfo> {
TestPlugin.info = testPluginInfo; TestPlugin.info = testPluginInfo;
} }
private static defaultTrialResult: Record<string, any> = { my: "result" }; static defaultTrialResult: Record<string, any> = { my: "result" };
static setDefaultTrialResult(defaultTrialResult: Record<string, any> = { my: "result" }) {
TestPlugin.defaultTrialResult = defaultTrialResult;
}
private static finishTrialMode: "immediate" | "manual" = "immediate"; private static finishTrialMode: "immediate" | "manual" = "immediate";
@ -38,8 +34,8 @@ class TestPlugin implements JsPsychPlugin<typeof testPluginInfo> {
} }
/** /**
* Makes the `trial` method of all instances of `TestPlugin` finish immediately and allows to manually finish the trial by * Makes the `trial` method of all instances of `TestPlugin` finish immediately and allows to
* invoking `TestPlugin.finishTrial()` instead. * manually finish the trial by invoking `TestPlugin.finishTrial()` instead.
*/ */
static setImmediateFinishTrialMode() { static setImmediateFinishTrialMode() {
TestPlugin.finishTrialMode = "immediate"; TestPlugin.finishTrialMode = "immediate";
@ -48,39 +44,19 @@ class TestPlugin implements JsPsychPlugin<typeof testPluginInfo> {
private static trialPromise = new PromiseWrapper<Record<string, any>>(); private static trialPromise = new PromiseWrapper<Record<string, any>>();
/** /**
* Resolves the promise returned by `trial()` with the provided `result` object or `{ my: "result" * Resolves the promise returned by `trial()` with the provided `result` or
* }` if no `result` object was provided. * `TestPlugin.defaultTrialResult` if no `result` object was passed.
**/ **/
static async finishTrial(result?: Record<string, any>) { static async finishTrial(result?: Record<string, any>) {
TestPlugin.trialPromise.resolve(result ?? TestPlugin.defaultTrialResult); TestPlugin.trialPromise.resolve(result ?? TestPlugin.defaultTrialResult);
await flushPromises(); await flushPromises();
} }
/** Resets all static properties including the `trial` function mock */ static defaultTrialImplementation(
static reset() {
TestPlugin.prototype.trial
.mockReset()
.mockImplementation(TestPlugin.prototype.defaultTrialImplementation);
TestPlugin.prototype.simulate
.mockReset()
.mockImplementation(TestPlugin.prototype.defaultSimulateImplementation);
this.resetPluginInfo();
this.setDefaultTrialResult();
this.setImmediateFinishTrialMode();
}
constructor(private jsPsych: JsPsych) {}
// For convenience, `trial` is set to a `jest.fn` below using `TestPlugin.prototype` and
// `defaultTrialImplementation`
trial: jest.Mock<Promise<TrialResult | void> | void>;
simulate: jest.Mock<Promise<TrialResult | void> | void>;
defaultTrialImplementation(
display_element: HTMLElement, display_element: HTMLElement,
trial: TrialType<typeof testPluginInfo>, trial: TrialType<typeof testPluginInfo>,
on_load: () => void on_load: () => void
) { ): void | Promise<TrialResult | void> {
on_load(); on_load();
if (TestPlugin.finishTrialMode === "immediate") { if (TestPlugin.finishTrialMode === "immediate") {
return Promise.resolve(TestPlugin.defaultTrialResult); return Promise.resolve(TestPlugin.defaultTrialResult);
@ -88,17 +64,32 @@ class TestPlugin implements JsPsychPlugin<typeof testPluginInfo> {
return TestPlugin.trialPromise.get(); return TestPlugin.trialPromise.get();
} }
defaultSimulateImplementation( public static trial = TestPlugin.defaultTrialImplementation;
static defaultSimulateImplementation(
trial: TrialType<typeof testPluginInfo>, trial: TrialType<typeof testPluginInfo>,
simulation_mode: SimulationMode, simulation_mode: SimulationMode,
simulation_options: SimulationOptions, simulation_options: SimulationOptions,
on_load?: () => void on_load?: () => void
): void | Promise<void | TrialResult> { ): void | Promise<void | TrialResult> {
return this.defaultTrialImplementation(document.createElement("div"), trial, on_load); return TestPlugin.defaultTrialImplementation(document.createElement("div"), trial, on_load);
} }
public static simulate = TestPlugin.defaultSimulateImplementation;
/** Resets all static properties including function implementations */
static reset() {
TestPlugin.defaultTrialResult = { my: "result" };
TestPlugin.trial = TestPlugin.defaultTrialImplementation;
TestPlugin.simulate = TestPlugin.defaultSimulateImplementation;
TestPlugin.resetPluginInfo();
TestPlugin.setImmediateFinishTrialMode();
}
constructor(private jsPsych: JsPsych) {}
trial = jest.fn(TestPlugin.trial);
simulate = jest.fn(TestPlugin.simulate);
} }
TestPlugin.prototype.trial = jest.fn(TestPlugin.prototype.defaultTrialImplementation);
TestPlugin.prototype.simulate = jest.fn(TestPlugin.prototype.defaultTrialImplementation);
export default TestPlugin; export default TestPlugin;

View File

@ -1,7 +1,7 @@
import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response"; import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response";
import { pressKey, startTimeline } from "@jspsych/test-utils"; import { pressKey, startTimeline } from "@jspsych/test-utils";
jest.useFakeTimers("modern"); jest.useFakeTimers();
describe("minimum_valid_rt parameter", () => { describe("minimum_valid_rt parameter", () => {
test("has a default value of 0", async () => { test("has a default value of 0", async () => {

View File

@ -30,7 +30,8 @@
"url": "https://github.com/jspsych/jsPsych/issues" "url": "https://github.com/jspsych/jsPsych/issues"
}, },
"peerDependencies": { "peerDependencies": {
"jspsych": ">=7.0.0" "jspsych": ">=7.0.0",
"@types/jest": "*"
}, },
"devDependencies": { "devDependencies": {
"@jspsych/config": "^1.1.0", "@jspsych/config": "^1.1.0",