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.
- 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.
- 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: |
${{ runner.os }}-node-${{ matrix.node }}-turbo-
- name: Check types
run: npm run tsc
- name: Build packages
run: npm run build
@ -44,8 +47,7 @@ jobs:
- name: Run tests
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
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v1

19795
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,21 +1,14 @@
const ts = require("typescript");
const { pathsToModuleNameMapper } = require("ts-jest");
const hq = require("alias-hq");
/** @type { (dirname: string) => import('@jest/types').Config.InitialOptions } */
module.exports.makePackageConfig = (dirname) => {
const packageJson = require(dirname + "/package.json");
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 {
preset: "ts-jest",
moduleNameMapper: pathsToModuleNameMapper(tsCompilerOptions.paths, { prefix: "<rootDir>/" }),
transform: { "\\.(js|jsx|ts|tsx)$": "@sucrase/jest-plugin" },
moduleNameMapper: hq.load(dirname + "/tsconfig.json").get("jest"),
testEnvironment: "jsdom",
testEnvironmentOptions: {
fetchExternalResources: true,

View File

@ -47,25 +47,27 @@
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "13.3.0",
"@rollup/plugin-replace": "4.0.0",
"@sucrase/jest-plugin": "3.0.0",
"@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",
"canvas": "2.9.1",
"gulp": "4.0.2",
"gulp-cli": "2.3.0",
"gulp-file": "^0.4.0",
"gulp-file": "0.4.0",
"gulp-rename": "2.0.0",
"gulp-replace": "1.1.3",
"gulp-zip": "5.1.0",
"jest": "28.1.0",
"jest-environment-jsdom": "28.1.0",
"jest": "29.3.1",
"jest-environment-jsdom": "29.3.1",
"merge-stream": "2.0.0",
"regenerator-runtime": "0.13.9",
"rollup": "2.73.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-typescript2": "0.31.2",
"ts-jest": "28.0.2",
"sucrase": "3.29.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": {
"target": "ES6",
"module": "ESNext",
"lib": ["dom", "esnext"],
"importHelpers": true,
// output .d.ts declaration files for consumers
"declaration": true,
// output .js.map sourcemap files for consumers
"sourceMap": true,
// stricter type-checking for stronger correctness. Recommended by TS
"strict": false, // should be enabled one lucky day
// linter checks for common issues
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedLocals": 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",
// interop between ESM and CJS modules. Recommended by TS
"esModuleInterop": true,
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
"skipLibCheck": true,
// error out if import and file system have a casing mismatch. Recommended by TS
"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", () => {
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 {
JsPsychPlugin,
PluginInfo,
TrialType,
ParameterType,
universalPluginParameters,
UniversalPluginParameters,
} from "./modules/plugins";
export { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions";
export type { JsPsychPlugin, PluginInfo, TrialType } from "./modules/plugins";
export { ParameterType } from "./modules/plugins";
export type { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions";

View File

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

View File

@ -106,13 +106,11 @@ describe("Trial", () => {
await trial1.run();
expect(trial1.getResult()).toEqual(expect.objectContaining({ my: "result" }));
jest
.spyOn(TestPlugin.prototype, "trial")
.mockImplementation(async (display_element, trial, on_load) => {
TestPlugin.trial = async (display_element, trial, on_load) => {
on_load();
dependencies.finishTrialPromise.resolve({ finishTrial: "result" });
return { my: "result" };
});
};
const trial2 = createTrial({ type: TestPlugin });
await trial2.run();
@ -122,9 +120,9 @@ describe("Trial", () => {
describe("if `trial` returns no promise", () => {
beforeAll(() => {
TestPlugin.prototype.trial.mockImplementation(() => {
TestPlugin.trial = () => {
dependencies.finishTrialPromise.resolve({ my: "result" });
});
};
});
it("invokes the local `on_load` callback", async () => {
@ -216,11 +214,11 @@ describe("Trial", () => {
complexArrayParameter: { type: ParameterType.COMPLEX, array: true },
functionParameter: { type: ParameterType.FUNCTION },
});
TestPlugin.setDefaultTrialResult({
TestPlugin.defaultTrialResult = {
result: "foo",
stringParameter2: "string",
stringParameter3: "string",
});
};
const trial = createTrial({
type: TestPlugin,
stringParameter1: "string",
@ -367,12 +365,12 @@ describe("Trial", () => {
await expect(
createTrial({ type: TestPlugin, stringArray: {} }).run()
).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(
createTrial({ type: TestPlugin, stringArray: 1 }).run()
).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;
}
private static defaultTrialResult: Record<string, any> = { my: "result" };
static setDefaultTrialResult(defaultTrialResult: Record<string, any> = { my: "result" }) {
TestPlugin.defaultTrialResult = defaultTrialResult;
}
static defaultTrialResult: Record<string, any> = { my: "result" };
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
* invoking `TestPlugin.finishTrial()` instead.
* Makes the `trial` method of all instances of `TestPlugin` finish immediately and allows to
* manually finish the trial by invoking `TestPlugin.finishTrial()` instead.
*/
static setImmediateFinishTrialMode() {
TestPlugin.finishTrialMode = "immediate";
@ -48,39 +44,19 @@ class TestPlugin implements JsPsychPlugin<typeof testPluginInfo> {
private static trialPromise = new PromiseWrapper<Record<string, any>>();
/**
* Resolves the promise returned by `trial()` with the provided `result` object or `{ my: "result"
* }` if no `result` object was provided.
* Resolves the promise returned by `trial()` with the provided `result` or
* `TestPlugin.defaultTrialResult` if no `result` object was passed.
**/
static async finishTrial(result?: Record<string, any>) {
TestPlugin.trialPromise.resolve(result ?? TestPlugin.defaultTrialResult);
await flushPromises();
}
/** Resets all static properties including the `trial` function mock */
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(
static defaultTrialImplementation(
display_element: HTMLElement,
trial: TrialType<typeof testPluginInfo>,
on_load: () => void
) {
): void | Promise<TrialResult | void> {
on_load();
if (TestPlugin.finishTrialMode === "immediate") {
return Promise.resolve(TestPlugin.defaultTrialResult);
@ -88,17 +64,32 @@ class TestPlugin implements JsPsychPlugin<typeof testPluginInfo> {
return TestPlugin.trialPromise.get();
}
defaultSimulateImplementation(
public static trial = TestPlugin.defaultTrialImplementation;
static defaultSimulateImplementation(
trial: TrialType<typeof testPluginInfo>,
simulation_mode: SimulationMode,
simulation_options: SimulationOptions,
on_load?: () => void
): void | Promise<void | TrialResult> {
return this.defaultTrialImplementation(document.createElement("div"), trial, on_load);
}
return TestPlugin.defaultTrialImplementation(document.createElement("div"), trial, on_load);
}
TestPlugin.prototype.trial = jest.fn(TestPlugin.prototype.defaultTrialImplementation);
TestPlugin.prototype.simulate = jest.fn(TestPlugin.prototype.defaultTrialImplementation);
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);
}
export default TestPlugin;

View File

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

View File

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