preactjs / preact

⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOM.
https://preactjs.com
MIT License
36.81k stars 1.95k forks source link

'Cannot add property __, object is not extensible' #3852

Closed mike-lischke closed 1 year ago

mike-lischke commented 1 year ago

Describe the bug

In a big application we switched from CRA to preact with vite. The app runs fine and now I want to fix the unit tests. However, I keep getting the error mentioned in the title when I run a render test like this:

    it.only("Test button click", async () => {
        const component = shallow(
            <Button onClick={buttonClick}>
                Test button
            </Button>,
        );
        expect(component).toBeTruthy();
        expect(component.text()).toEqual("Test button");

        const click = (component.props() as IButtonProperties).onClick;
        await act(() => {
            click?.(mouseEventMock, { id: "1" });
        });

        expect(clicked).toEqual(true);
    });

The shallow call throws the error (which I know comes from enzyme), but I debugged it down to preact code. More precisely it happens in preact.module.js. I only have the node module at hand, so I don't know to which source file this error location corresponds. It's line 102 in the latest code:

function w(n, l, u, i, t, o, r, c, s, a) {
    var h, y, d, k, b, g, w, x = i && i.__k || e, C = x.length;
    for(u.__k = [], h = 0; h < l.length; h++)if (null != (k = u.__k[h] = null == (k = l[h]) || "boolean" == typeof k ? null : "string" == typeof k || "number" == typeof k || "bigint" == typeof k ? v(null, k, null, null, k) : Array.isArray(k) ? v(p, {
        children: k
    }, null, null, null) : k.__b > 0 ? v(k.type, k.props, k.key, k.ref ? k.ref : null, k.__v) : k)) {
[line 102]        if (k.__ = u, k.__b = u.__b + 1, null === (d = x[h]) || d && k.key == d.key && k.type === d.type) x[h] = void 0;
        else for(y = 0; y < C; y++){
            if ((d = x[y]) && k.key == d.key && k.type === d.type) {
                x[y] = void 0;
                break;
            }
            d = null;
        }
        j(n, k, d = d || f, t, o, r, c, s, a), b = k.__e, (y = k.ref) && d.ref != y && (w || (w = []), d.ref && w.push(d.ref, null, k), w.push(y, k.__c || b, k)), null != b ? (null == g && (g = b), "function" == typeof k.type && k.__k === d.__k ? k.__d = s = m(k, s, n) : s = A(n, k, d, x, b, s), "function" == typeof u.type && (u.__d = s)) : s && d.__e == s && s.parentNode != n && (s = _(d));
    }
    for(u.__e = g, h = C; h--;)null != x[h] && N(x[h], x[h]);
    if (w) for(h = 0; h < w.length; h++)M(w[h], w[++h], w[++h]);
}

I believe it's the first assignment. Here are the values:

k
{$$typeof: Symbol(react.element), type: ƒ, key: null, ref: null, props: {…}, …}
_owner: null
_store: {validated: false}
$$typeof: Symbol(react.element)
key: null
props: {onClick: ƒ, children: 'Test button'}
ref: null
type: class Button
_self: undefined
_source: undefined
[[Prototype]]: Object

u
{type: ƒ, props: {…}, key: undefined, ref: undefined, __k: Array(1), …}
__: null
__b: 0
__c: d {props: {…}, context: {…}, constructor: ƒ, render: ƒ, state: {…}, …}
__d: undefined
__e: null
__h: null
__k: (1) [{…}]
__v: 1
$$typeof: Symbol(react.element)
constructor: undefined
key: undefined
props: {children: Array(1)}
ref: undefined
type: ƒ p(n) {\n    return n.children;\n}
[[Prototype]]: Object

To Reproduce

I would love to give you a simple example for this problem, but this is a big project and I'm not yet done with fixing things, let alone extracting something from it to show a simple case.

All I do is to run this single test case.

Expected behavior

No error. The test should succeed as it did before I started converting the entire app. For completeness here's my jest config:

export default {
    // All imported modules in your tests should be mocked automatically
    // automock: false,

    // Stop running tests after `n` failures
    bail: 1,

    // Indicates whether the coverage information should be collected while executing the test
    collectCoverage: true,

    // An array of glob patterns indicating a set of files for which coverage information should be collected
    collectCoverageFrom: [
        "src/**/*.{ts,tsx}",
        "!src/tests/**",
        "!**/node_modules/**",
        "!src/app-wrapper/**",
        "!src/assets/**",
        "!src/parsing/mysql/generated/**",
        "!src/parsing/SQLite/generated/**",
        "!src/parsing/python/generated/**",
        "!src/**/*.d.ts"
    ],

    // The directory where Jest should output its coverage files
    coverageDirectory: "coverage",

    // Indicates which provider should be used to instrument code for coverage
    coverageProvider: "v8",

    // A list of reporter names that Jest uses when writing coverage reports
    coverageReporters: [
        "json",
        "text",
        "clover",
        "html"
    ],

    // An array of file extensions your modules use
    moduleFileExtensions: [
        "tsx",
        "ts",
        "js",
        "mjs",
        "cjs",
        "jsx",
        "json",
        "node"
    ],

    // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
    moduleNameMapper: {
        "monaco-editor$": "monaco-editor/esm/vs/editor/editor.api",
        "tabulator-tables$": "tabulator-tables/dist/js/tabulator_esm.js",
        ".+\\.worker\\?worker$": "<rootDir>/src/tests/unit-tests/__mocks__/workerMock.ts",
        ".+runtime\\.d\\.ts$": "<rootDir>/src/tests/unit-tests/__mocks__/typingsMock.ts"
    },

    // Automatically reset mock state before every test
    resetMocks: false,

    // A list of paths to directories that Jest should use to search for files in
    roots: [
        "src/tests/unit-tests"
    ],

    // A list of paths to modules that run some code to configure or set up the testing framework before each test
    setupFilesAfterEnv: [
        // Note: this is not optimal. This setup is run again for every test file, while we actually want to
        // run it only once.
        "./src/tests/setupUnitTests.ts",
    ],

    // A list of paths to snapshot serializer modules Jest should use for snapshot testing
    snapshotSerializers: [
        "enzyme-to-json/serializer"
    ],

    // The test environment that will be used for testing
    testEnvironment: "jsdom",

    // The glob patterns Jest uses to detect test files
    testMatch: [
        "**/tests/unit-tests/**/*.spec.[jt]s?(x)"
    ],

    // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
    testPathIgnorePatterns: [
        "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
        "^.+\\.module\\.(css|sass|scss)$",
        //   "/node_modules/"
    ],

    // A map from regular expressions to paths to transformers
    transform: {
        "^.+\\.(ts|js|tsx|jsx)$": "@swc/jest",
        "^.+\\.css$": "<rootDir>/src/tests/cssTransform.cjs",
        "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/src/tests/fileTransform.cjs",
    },

    // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
    transformIgnorePatterns: [
        "/node_modules/(?!monaco-editor)/",
        "/node_modules/(?!tabulator-tables)/",
        //"/node_modules/(?!preact)/"
    ],
};

and my project dependencies:


    "dependencies": {
        "ansi-to-react": "6.1.6",
        "antlr4-c3": "2.2.1",
        "antlr4ts": "0.5.0-alpha.4",
        "classnames": "2.3.2",
        "color": "4.2.3",
        "d3": "7.8.0",
        "idb": "7.1.1",
        "keyboard-key": "1.1.0",
        "monaco-editor": "0.34.1",
        "preact": "10.11.3",
        "set-interval-async": "3.0.3",
        "tabulator-tables": "5.4.3",
        "ws": "8.11.0"
    },
    "devDependencies": {
        "@esbuild-plugins/node-globals-polyfill": "0.1.1",
        "@esbuild-plugins/node-modules-polyfill": "0.1.4",
        "@preact/preset-vite": "2.5.0",
        "@rollup/plugin-node-resolve": "15.0.1",
        "@swc/core": "1.3.24",
        "@swc/jest": "0.2.24",
        "@testing-library/jest-dom": "5.16.5",
        "@testing-library/preact": "3.2.2",
        "@types/color": "3.0.3",
        "@types/d3": "7.4.0",
        "@types/enzyme": "3.10.12",
        "@types/jest": "29.2.5",
        "@types/jsdom": "20.0.1",
        "@types/lodash": "4.14.191",
        "@types/node": "18.11.18",
        "@types/resize-observer-browser": "0.1.7",
        "@types/selenium-webdriver": "4.1.10",
        "@types/set-interval-async": "1.0.0",
        "@types/tabulator-tables": "5.4.2",
        "@types/ws": "8.5.4",
        "@typescript-eslint/eslint-plugin": "5.48.0",
        "@typescript-eslint/parser": "5.48.0",
        "antlr4ts-cli": "0.5.0-alpha.4",
        "enzyme": "3.11.0",
        "enzyme-adapter-preact-pure": "4.1.0",
        "enzyme-to-json": "3.6.2",
        "es5-ext": "0.10.53",
        "eslint": "8.31.0",
        "eslint-config-preact": "1.3.0",
        "eslint-plugin-import": "2.26.0",
        "eslint-plugin-jsdoc": "39.6.4",
        "eslint-plugin-jsx-a11y": "6.6.1",
        "eslint-plugin-prefer-arrow": "1.2.3",
        "fake-indexeddb": "4.0.1",
        "identity-obj-proxy": "3.0.0",
        "jest": "29.3.1",
        "jest-canvas-mock": "2.4.0",
        "jest-environment-jsdom": "29.3.1",
        "jest-html-reporter": "3.7.0",
        "monaco-typescript": "4.10.0",
        "mysql-promise": "5.0.0",
        "oci-bastion": "2.50.0",
        "oci-common": "2.50.0",
        "oci-core": "2.50.0",
        "oci-identity": "2.50.0",
        "oci-loadbalancer": "2.50.0",
        "oci-mysql": "2.50.0",
        "rollup-plugin-polyfill-node": "0.11.0",
        "run-my-sql-file": "1.0.0",
        "selenium-webdriver": "4.7.1",
        "ts-node": "10.9.1",
        "typescript": "4.9.4",
        "vite": "4.0.4",
        "vite-plugin-monaco-editor": "1.1.0"
    },
mike-lischke commented 1 year ago

Well, it's actually a problem with Jest, which uses React for running tests. The solution for the above problem is to map the React calls to preact, like:

    moduleNameMapper: {
        '^react/jsx-runtime$': 'preact/jsx-runtime',
    },

in jest.config.js or package.json. It's not clear to me why any React runtime call would be used without this mapping, given that I have no React installed.