From dd24ff73716fd26a24e0f84b6433373bda37f7f6 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 29 Aug 2023 15:10:03 +0200 Subject: [PATCH 01/12] example testing helpers with `using` --- .prettierignore | 1 + package-lock.json | 412 ++++++++++++++++++++++++---- package.json | 6 +- src/config/jest/setup.ts | 13 +- src/testing/__tests__/using.test.ts | 85 ++++++ 5 files changed, 462 insertions(+), 55 deletions(-) create mode 100644 src/testing/__tests__/using.test.ts diff --git a/.prettierignore b/.prettierignore index e7cd8b12c39..1ba060fc649 100644 --- a/.prettierignore +++ b/.prettierignore @@ -36,6 +36,7 @@ node_modules/ # Ignore all files in /scripts directory /scripts +!/src /src/__tests__/ApolloClient.ts /src/__tests__/client.ts /src/__tests__/exports.ts diff --git a/package-lock.json b/package-lock.json index d9fc86840c6..405d39495e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,8 +48,8 @@ "@types/react": "18.2.21", "@types/react-dom": "18.2.7", "@types/use-sync-external-store": "0.0.3", - "@typescript-eslint/eslint-plugin": "5.62.0", - "@typescript-eslint/parser": "5.62.0", + "@typescript-eslint/eslint-plugin": "6.5.0", + "@typescript-eslint/parser": "6.5.0", "acorn": "8.10.0", "blob-polyfill": "7.0.20220408", "bytes": "3.1.2", @@ -68,7 +68,7 @@ "jest-junit": "16.0.0", "lodash": "4.17.21", "patch-package": "7.0.2", - "prettier": "3.0.2", + "prettier": "3.0.3", "react": "18.2.0", "react-17": "npm:react@^17", "react-dom": "18.2.0", @@ -1285,9 +1285,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", - "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" @@ -2682,9 +2682,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, "node_modules/@types/json5": { @@ -2837,32 +2837,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.5.0.tgz", + "integrity": "sha512-2pktILyjvMaScU6iK3925uvGU87E+N9rh372uGZgiMYwafaw9SXq86U04XPq3UH6tzRvNgBsub6x2DacHc33lw==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.5.0", + "@typescript-eslint/type-utils": "6.5.0", + "@typescript-eslint/utils": "6.5.0", + "@typescript-eslint/visitor-keys": "6.5.0", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2870,6 +2871,105 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz", + "integrity": "sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/visitor-keys": "6.5.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.5.0.tgz", + "integrity": "sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz", + "integrity": "sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/visitor-keys": "6.5.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.5.0.tgz", + "integrity": "sha512-9nqtjkNykFzeVtt9Pj6lyR9WEdd8npPhhIPM992FWVkZuS6tmxHfGVnlUcjpUP2hv8r4w35nT33mlxd+Be1ACQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.5.0", + "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/typescript-estree": "6.5.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz", + "integrity": "sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.5.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -2886,25 +2986,83 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.5.0.tgz", + "integrity": "sha512-LMAVtR5GN8nY0G0BadkG0XIe4AcNMeyEy3DyhKGAh9k4pLSMBO7rF29JvDBpZGCmp5Pgz5RLHP6eCpSYZJQDuQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "6.5.0", + "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/typescript-estree": "6.5.0", + "@typescript-eslint/visitor-keys": "6.5.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz", + "integrity": "sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/visitor-keys": "6.5.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.5.0.tgz", + "integrity": "sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz", + "integrity": "sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/visitor-keys": "6.5.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependenciesMeta": { "typescript": { @@ -2912,6 +3070,38 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz", + "integrity": "sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.5.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", @@ -2930,25 +3120,82 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.5.0.tgz", + "integrity": "sha512-f7OcZOkRivtujIBQ4yrJNIuwyCQO1OjocVqntl9dgSIZAdKqicj3xFDqDOzHDlGCZX990LqhLQXWRnQvsapq8A==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.5.0", + "@typescript-eslint/utils": "6.5.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz", + "integrity": "sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/visitor-keys": "6.5.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.5.0.tgz", + "integrity": "sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz", + "integrity": "sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/visitor-keys": "6.5.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependenciesMeta": { "typescript": { @@ -2956,6 +3203,63 @@ } } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.5.0.tgz", + "integrity": "sha512-9nqtjkNykFzeVtt9Pj6lyR9WEdd8npPhhIPM992FWVkZuS6tmxHfGVnlUcjpUP2hv8r4w35nT33mlxd+Be1ACQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.5.0", + "@typescript-eslint/types": "6.5.0", + "@typescript-eslint/typescript-estree": "6.5.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz", + "integrity": "sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.5.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/types": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", @@ -5980,9 +6284,9 @@ } }, "node_modules/ignore": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", - "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true, "engines": { "node": ">= 4" @@ -8290,12 +8594,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -9010,9 +9308,9 @@ } }, "node_modules/prettier": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz", - "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -10445,6 +10743,18 @@ "node": ">=8" } }, + "node_modules/ts-api-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", + "integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-invariant": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", diff --git a/package.json b/package.json index f224fa1b1a3..08c696b9847 100644 --- a/package.json +++ b/package.json @@ -123,8 +123,8 @@ "@types/react": "18.2.21", "@types/react-dom": "18.2.7", "@types/use-sync-external-store": "0.0.3", - "@typescript-eslint/eslint-plugin": "5.62.0", - "@typescript-eslint/parser": "5.62.0", + "@typescript-eslint/eslint-plugin": "6.5.0", + "@typescript-eslint/parser": "6.5.0", "acorn": "8.10.0", "blob-polyfill": "7.0.20220408", "bytes": "3.1.2", @@ -143,7 +143,7 @@ "jest-junit": "16.0.0", "lodash": "4.17.21", "patch-package": "7.0.2", - "prettier": "3.0.2", + "prettier": "3.0.3", "react": "18.2.0", "react-17": "npm:react@^17", "react-dom": "18.2.0", diff --git a/src/config/jest/setup.ts b/src/config/jest/setup.ts index b625bfd3d08..6828f0d446f 100644 --- a/src/config/jest/setup.ts +++ b/src/config/jest/setup.ts @@ -15,4 +15,15 @@ function fail(reason = "fail was called in a test.") { } // @ts-ignore -globalThis.fail = fail; \ No newline at end of file +globalThis.fail = fail; + +if (!Symbol.dispose) { + Object.defineProperty(Symbol, 'dispose', { + value: Symbol('dispose'), + }) +} +if (!Symbol.asyncDispose) { + Object.defineProperty(Symbol, 'asyncDispose', { + value: Symbol('asyncDispose'), + }) +} \ No newline at end of file diff --git a/src/testing/__tests__/using.test.ts b/src/testing/__tests__/using.test.ts new file mode 100644 index 00000000000..69af8a37da4 --- /dev/null +++ b/src/testing/__tests__/using.test.ts @@ -0,0 +1,85 @@ +type NoInfer = [T][T extends any ? 0 : never]; + +function defineDisposable( + create: (...args: Args) => T, + cleanup: (object: NoInfer, ...args: NoInfer) => void +) { + return function (...args: Args): T & Disposable { + const obj = create(...args); + return Object.assign(obj, { + [Symbol.dispose]() { + cleanup.call(undefined, obj, args); + }, + }); + }; +} + +const mockedConsoleMethods = ["log", "info", "warn", "error", "debug"] as const; +const spyOnConsole = defineDisposable( + () => { + let originalMethods = { ...console }; + let calls: Record<(typeof mockedConsoleMethods)[number], any[][]> = + {} as any; + for (const key of mockedConsoleMethods) { + calls[key] = []; + console[key] = (...args: any[]) => { + calls[key].push(args); + }; + } + return { originalMethods, ...calls }; + }, + (object) => { + for (const key of mockedConsoleMethods) { + console[key] = object.originalMethods[key]; + } + } +); + +describe("defineDisposable", () => { + it("calls cleanup", () => { + let cleanedUp = false; + const createDisposable = defineDisposable( + () => { + return {}; + }, + () => { + cleanedUp = true; + } + ); + { + using x = createDisposable(); + } + expect(cleanedUp).toBe(true); + }); +}); + +describe("spyOnConsole", () => { + test("intercepts calls to `console.log` and `console.info`", () => { + using mockedConsole = spyOnConsole(); + console.log("hello"); + console.info("world"); + expect(mockedConsole.log).toEqual([["hello"]]); + expect(mockedConsole.info).toEqual([["world"]]); + }); + + test("restores the original `console` methods", () => { + const originalLog = console.log; + const originalWarn = console.warn; + const originalError = console.error; + const originalDebug = console.debug; + const originalInfo = console.info; + { + using _x = spyOnConsole(); + expect(console.log).not.toBe(originalLog); + expect(console.warn).not.toBe(originalWarn); + expect(console.error).not.toBe(originalError); + expect(console.debug).not.toBe(originalDebug); + expect(console.info).not.toBe(originalInfo); + } + expect(console.log).toBe(originalLog); + expect(console.warn).toBe(originalWarn); + expect(console.error).toBe(originalError); + expect(console.debug).toBe(originalDebug); + expect(console.info).toBe(originalInfo); + }); +}); From 65e31716c7f363158309e990946f73e410c73837 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 7 Sep 2023 13:17:06 +0200 Subject: [PATCH 02/12] eslint rule, broken modules --- config/eslint-rules/fixtures/file.ts | 0 config/eslint-rules/fixtures/react.tsx | 0 config/eslint-rules/fixtures/tsconfig.json | 6 + config/eslint-rules/package.json | 6 + .../eslint-rules/require-using-disposable.mjs | 55 ++ .../require-using-disposable.test.mjs | 45 ++ config/eslint-rules/tsconfig.json | 6 + package-lock.json | 489 ++++-------------- package.json | 10 +- 9 files changed, 219 insertions(+), 398 deletions(-) create mode 100644 config/eslint-rules/fixtures/file.ts create mode 100644 config/eslint-rules/fixtures/react.tsx create mode 100644 config/eslint-rules/fixtures/tsconfig.json create mode 100644 config/eslint-rules/package.json create mode 100644 config/eslint-rules/require-using-disposable.mjs create mode 100644 config/eslint-rules/require-using-disposable.test.mjs create mode 100644 config/eslint-rules/tsconfig.json diff --git a/config/eslint-rules/fixtures/file.ts b/config/eslint-rules/fixtures/file.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/config/eslint-rules/fixtures/react.tsx b/config/eslint-rules/fixtures/react.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/config/eslint-rules/fixtures/tsconfig.json b/config/eslint-rules/fixtures/tsconfig.json new file mode 100644 index 00000000000..c7b29c1143d --- /dev/null +++ b/config/eslint-rules/fixtures/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "strict": true + }, + "include": ["file.ts", "react.tsx"] +} diff --git a/config/eslint-rules/package.json b/config/eslint-rules/package.json new file mode 100644 index 00000000000..fb57e11ceef --- /dev/null +++ b/config/eslint-rules/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "scripts": { + "test": "node *.test.mjs" + } +} diff --git a/config/eslint-rules/require-using-disposable.mjs b/config/eslint-rules/require-using-disposable.mjs new file mode 100644 index 00000000000..08cd20426a1 --- /dev/null +++ b/config/eslint-rules/require-using-disposable.mjs @@ -0,0 +1,55 @@ +// @ts-check +import { ESLintUtils } from "@typescript-eslint/utils"; +import { unionTypeParts, isObjectType } from "ts-api-tools"; + +export const rule = ESLintUtils.RuleCreator.withoutDocs({ + create(context) { + return { + VariableDeclaration(node) { + for (const declarator of node.declarations) { + if (!declarator.init) continue; + const services = ESLintUtils.getParserServices(context); + const type = services.getTypeAtLocation(declarator.init); + for (const typePart of unionTypeParts(type)) { + if (!isObjectType(typePart)) { + continue; + } + if ( + // bad check, but will do for now + // in the future, we should check for a `[Symbol.disposable]` property + // but I have no idea how to use that right now + typePart.symbol.escapedName === "Disposable" && + node.kind != "using" + ) { + context.report({ + messageId: "missingUsing", + node: declarator, + }); + } + if ( + // bad check + typePart.symbol.escapedName === "AsyncDisposable" && + node.kind != "await using" + ) { + context.report({ + messageId: "missingAwaitUsing", + node: declarator, + }); + } + } + } + }, + }; + }, + meta: { + messages: { + missingUsing: + "Disposables should be allocated with `using `.", + missingAwaitUsing: + "AsyncDisposables should be allocated with `await using `.", + }, + type: "suggestion", + schema: [], + }, + defaultOptions: [], +}); diff --git a/config/eslint-rules/require-using-disposable.test.mjs b/config/eslint-rules/require-using-disposable.test.mjs new file mode 100644 index 00000000000..f799d1b9956 --- /dev/null +++ b/config/eslint-rules/require-using-disposable.test.mjs @@ -0,0 +1,45 @@ +// @ts-check +import { RuleTester } from "@typescript-eslint/rule-tester"; +import { rule } from "./require-using-disposable.mjs"; +import nodeTest from "node:test"; + +RuleTester.it = nodeTest.it; +RuleTester.itOnly = nodeTest.only; +RuleTester.describe = nodeTest.describe; +RuleTester.afterAll = nodeTest.after; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname + "/fixtures", + }, +}); +ruleTester.run("my-typed-rule", rule, { + valid: [ + ` + function foo(): Disposable {} + using bar = foo() + `, + ` + function foo(): AsyncDisposable {} + await using bar = foo() + `, + ], + invalid: [ + { + code: ` + function foo(): Disposable {} + const bar = foo() + `, + errors: [{ messageId: "missingUsing" }], + }, + { + code: ` + function foo(): AsyncDisposable {} + const bar = foo() + `, + errors: [{ messageId: "missingAwaitUsing" }], + }, + ], +}); diff --git a/config/eslint-rules/tsconfig.json b/config/eslint-rules/tsconfig.json new file mode 100644 index 00000000000..1bebd83349c --- /dev/null +++ b/config/eslint-rules/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "allowJs": true + } +} diff --git a/package-lock.json b/package-lock.json index 5c8f9822f20..5dbbf8a653c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@testing-library/react": "14.0.0", "@testing-library/react-12": "npm:@testing-library/react@^12", "@testing-library/user-event": "14.4.3", + "@tsconfig/node20": "20.1.2", "@types/bytes": "3.1.1", "@types/fetch-mock": "7.3.5", "@types/glob": "8.1.0", @@ -48,8 +49,11 @@ "@types/react": "18.2.21", "@types/react-dom": "18.2.7", "@types/use-sync-external-store": "0.0.3", - "@typescript-eslint/eslint-plugin": "6.5.0", - "@typescript-eslint/parser": "6.5.0", + "@typescript-eslint/eslint-plugin": "6.6.0", + "@typescript-eslint/parser": "6.6.0", + "@typescript-eslint/rule-tester": "^6.6.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/utils": "6.6.0", "acorn": "8.10.0", "blob-polyfill": "7.0.20220408", "bytes": "3.1.2", @@ -57,7 +61,6 @@ "eslint": "8.48.0", "eslint-import-resolver-typescript": "3.6.0", "eslint-plugin-import": "npm:@phryneas/eslint-plugin-import@2.27.5-pr.2813.2817.199971c", - "eslint-plugin-testing-library": "5.11.1", "expect-type": "0.16.0", "fetch-mock": "9.11.0", "glob": "8.1.0", @@ -83,6 +86,7 @@ "size-limit": "8.2.6", "subscriptions-transport-ws": "0.11.0", "terser": "5.19.2", + "ts-api-tools": "^0.0.20", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", "ts-node": "10.9.1", @@ -2507,6 +2511,12 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "node_modules/@tsconfig/node20": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.2.tgz", + "integrity": "sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==", + "dev": true + }, "node_modules/@types/aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", @@ -2837,16 +2847,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.5.0.tgz", - "integrity": "sha512-2pktILyjvMaScU6iK3925uvGU87E+N9rh372uGZgiMYwafaw9SXq86U04XPq3UH6tzRvNgBsub6x2DacHc33lw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz", + "integrity": "sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.5.0", - "@typescript-eslint/type-utils": "6.5.0", - "@typescript-eslint/utils": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0", + "@typescript-eslint/scope-manager": "6.6.0", + "@typescript-eslint/type-utils": "6.6.0", + "@typescript-eslint/utils": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2871,105 +2881,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz", - "integrity": "sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.5.0.tgz", - "integrity": "sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz", - "integrity": "sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.5.0.tgz", - "integrity": "sha512-9nqtjkNykFzeVtt9Pj6lyR9WEdd8npPhhIPM992FWVkZuS6tmxHfGVnlUcjpUP2hv8r4w35nT33mlxd+Be1ACQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.5.0", - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/typescript-estree": "6.5.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz", - "integrity": "sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.5.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -2986,15 +2897,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.5.0.tgz", - "integrity": "sha512-LMAVtR5GN8nY0G0BadkG0XIe4AcNMeyEy3DyhKGAh9k4pLSMBO7rF29JvDBpZGCmp5Pgz5RLHP6eCpSYZJQDuQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.6.0.tgz", + "integrity": "sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.5.0", - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/typescript-estree": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0", + "@typescript-eslint/scope-manager": "6.6.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0", "debug": "^4.3.4" }, "engines": { @@ -3013,49 +2924,17 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz", - "integrity": "sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==", + "node_modules/@typescript-eslint/rule-tester": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-6.6.0.tgz", + "integrity": "sha512-eKxBRBOQbReGr1g+CFjKbK3XyVyBlkZV0ur1PJ3SXwsW3/fg4w6lA41GnnS2IBD15PUXRTAZ7AFBfvpoGfJIXw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.5.0.tgz", - "integrity": "sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz", - "integrity": "sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/utils": "6.6.0", + "ajv": "^6.10.0", + "lodash.merge": "4.6.2", + "semver": "^7.5.4" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3064,30 +2943,12 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz", - "integrity": "sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.5.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "@eslint/eslintrc": ">=2", + "eslint": ">=8" } }, - "node_modules/@typescript-eslint/parser/node_modules/semver": { + "node_modules/@typescript-eslint/rule-tester/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", @@ -3103,16 +2964,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz", + "integrity": "sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -3120,13 +2981,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.5.0.tgz", - "integrity": "sha512-f7OcZOkRivtujIBQ4yrJNIuwyCQO1OjocVqntl9dgSIZAdKqicj3xFDqDOzHDlGCZX990LqhLQXWRnQvsapq8A==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz", + "integrity": "sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.5.0", - "@typescript-eslint/utils": "6.5.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/utils": "6.6.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -3146,27 +3007,10 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz", - "integrity": "sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.5.0.tgz", - "integrity": "sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==", + "node_modules/@typescript-eslint/types": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz", + "integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3176,14 +3020,14 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz", - "integrity": "sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz", + "integrity": "sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/visitor-keys": "6.5.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3203,103 +3047,6 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.5.0.tgz", - "integrity": "sha512-9nqtjkNykFzeVtt9Pj6lyR9WEdd8npPhhIPM992FWVkZuS6tmxHfGVnlUcjpUP2hv8r4w35nT33mlxd+Be1ACQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.5.0", - "@typescript-eslint/types": "6.5.0", - "@typescript-eslint/typescript-estree": "6.5.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz", - "integrity": "sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.5.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -3316,57 +3063,34 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.6.0.tgz", + "integrity": "sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.6.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3379,16 +3103,16 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz", + "integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.6.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -5160,22 +4884,6 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-testing-library": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", - "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "^5.58.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6" - }, - "peerDependencies": { - "eslint": "^7.5.0 || ^8.0.0" - } - }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -10743,6 +10451,18 @@ "node": ">=8" } }, + "node_modules/ts-api-tools": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/ts-api-tools/-/ts-api-tools-0.0.20.tgz", + "integrity": "sha512-cUwPor7VxMQt8M2tsmfgTehNz4b03wIbJEiBUFWoSvVePNHCTuYNjWo/9jT14qeJQvouXknl81POjrR0QzMQeA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": ">=4" + } + }, "node_modules/ts-api-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", @@ -10923,27 +10643,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/tty-table": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.6.tgz", diff --git a/package.json b/package.json index 8b10bdc4913..42e06d34ca5 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "@testing-library/react": "14.0.0", "@testing-library/react-12": "npm:@testing-library/react@^12", "@testing-library/user-event": "14.4.3", + "@tsconfig/node20": "20.1.2", "@types/bytes": "3.1.1", "@types/fetch-mock": "7.3.5", "@types/glob": "8.1.0", @@ -125,8 +126,11 @@ "@types/react": "18.2.21", "@types/react-dom": "18.2.7", "@types/use-sync-external-store": "0.0.3", - "@typescript-eslint/eslint-plugin": "6.5.0", - "@typescript-eslint/parser": "6.5.0", + "@typescript-eslint/eslint-plugin": "6.6.0", + "@typescript-eslint/parser": "6.6.0", + "@typescript-eslint/rule-tester": "^6.6.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/utils": "6.6.0", "acorn": "8.10.0", "blob-polyfill": "7.0.20220408", "bytes": "3.1.2", @@ -134,7 +138,6 @@ "eslint": "8.48.0", "eslint-import-resolver-typescript": "3.6.0", "eslint-plugin-import": "npm:@phryneas/eslint-plugin-import@2.27.5-pr.2813.2817.199971c", - "eslint-plugin-testing-library": "5.11.1", "expect-type": "0.16.0", "fetch-mock": "9.11.0", "glob": "8.1.0", @@ -160,6 +163,7 @@ "size-limit": "8.2.6", "subscriptions-transport-ws": "0.11.0", "terser": "5.19.2", + "ts-api-tools": "^0.0.20", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", "ts-node": "10.9.1", From 353b5dcc06758d94a44374d9957d2c4c556dfc73 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 7 Sep 2023 15:23:31 +0200 Subject: [PATCH 03/12] working local eslint rule --- .eslintrc | 13 +- config/eslint-rules/package.json | 6 - .../require-using-disposable.test.mjs | 45 ----- .../fixtures/file.ts | 0 .../fixtures/react.tsx | 0 .../fixtures/tsconfig.json | 0 eslint-local-rules/index.js | 14 ++ eslint-local-rules/package.json | 5 + .../require-using-disposable.test.ts | 31 +++ .../require-using-disposable.ts | 8 +- eslint-local-rules/testSetup.ts | 15 ++ .../tsconfig.json | 3 +- package-lock.json | 184 +++++++++++++++++- package.json | 4 +- tsconfig.tests.json | 5 + 15 files changed, 271 insertions(+), 62 deletions(-) delete mode 100644 config/eslint-rules/package.json delete mode 100644 config/eslint-rules/require-using-disposable.test.mjs rename {config/eslint-rules => eslint-local-rules}/fixtures/file.ts (100%) rename {config/eslint-rules => eslint-local-rules}/fixtures/react.tsx (100%) rename {config/eslint-rules => eslint-local-rules}/fixtures/tsconfig.json (100%) create mode 100644 eslint-local-rules/index.js create mode 100644 eslint-local-rules/package.json create mode 100644 eslint-local-rules/require-using-disposable.test.ts rename config/eslint-rules/require-using-disposable.mjs => eslint-local-rules/require-using-disposable.ts (88%) create mode 100644 eslint-local-rules/testSetup.ts rename {config/eslint-rules => eslint-local-rules}/tsconfig.json (65%) create mode 100644 tsconfig.tests.json diff --git a/.eslintrc b/.eslintrc index 46a5a102e43..d84316f239d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,6 @@ { "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint", "import"], + "plugins": ["@typescript-eslint", "import", "local-rules"], "env": { "browser": true, "node": true, @@ -52,15 +52,20 @@ "ignorePackages": true, "checkTypeImports": true } - ] + ], + "local-rules/require-using-disposable": "error" } }, { - "files": ["**/__tests__/**/*.[jt]sx", "**/?(*.)+(test).[jt]sx"], + "files": ["**/__tests__/**/*.[jt]s", "**/__tests__/**/*.[jt]sx", "**/?(*.)+(test).[jt]s", "**/?(*.)+(test).[jt]sx"], "extends": ["plugin:testing-library/react"], + "parserOptions": { + "project": "./tsconfig.tests.json" + }, "rules": { "testing-library/prefer-user-event": "error", - "testing-library/no-wait-for-multiple-assertions": "off" + "testing-library/no-wait-for-multiple-assertions": "off", + "local-rules/require-using-disposable": "error" } } ], diff --git a/config/eslint-rules/package.json b/config/eslint-rules/package.json deleted file mode 100644 index fb57e11ceef..00000000000 --- a/config/eslint-rules/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "module", - "scripts": { - "test": "node *.test.mjs" - } -} diff --git a/config/eslint-rules/require-using-disposable.test.mjs b/config/eslint-rules/require-using-disposable.test.mjs deleted file mode 100644 index f799d1b9956..00000000000 --- a/config/eslint-rules/require-using-disposable.test.mjs +++ /dev/null @@ -1,45 +0,0 @@ -// @ts-check -import { RuleTester } from "@typescript-eslint/rule-tester"; -import { rule } from "./require-using-disposable.mjs"; -import nodeTest from "node:test"; - -RuleTester.it = nodeTest.it; -RuleTester.itOnly = nodeTest.only; -RuleTester.describe = nodeTest.describe; -RuleTester.afterAll = nodeTest.after; - -const ruleTester = new RuleTester({ - parser: "@typescript-eslint/parser", - parserOptions: { - project: "./tsconfig.json", - tsconfigRootDir: __dirname + "/fixtures", - }, -}); -ruleTester.run("my-typed-rule", rule, { - valid: [ - ` - function foo(): Disposable {} - using bar = foo() - `, - ` - function foo(): AsyncDisposable {} - await using bar = foo() - `, - ], - invalid: [ - { - code: ` - function foo(): Disposable {} - const bar = foo() - `, - errors: [{ messageId: "missingUsing" }], - }, - { - code: ` - function foo(): AsyncDisposable {} - const bar = foo() - `, - errors: [{ messageId: "missingAwaitUsing" }], - }, - ], -}); diff --git a/config/eslint-rules/fixtures/file.ts b/eslint-local-rules/fixtures/file.ts similarity index 100% rename from config/eslint-rules/fixtures/file.ts rename to eslint-local-rules/fixtures/file.ts diff --git a/config/eslint-rules/fixtures/react.tsx b/eslint-local-rules/fixtures/react.tsx similarity index 100% rename from config/eslint-rules/fixtures/react.tsx rename to eslint-local-rules/fixtures/react.tsx diff --git a/config/eslint-rules/fixtures/tsconfig.json b/eslint-local-rules/fixtures/tsconfig.json similarity index 100% rename from config/eslint-rules/fixtures/tsconfig.json rename to eslint-local-rules/fixtures/tsconfig.json diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js new file mode 100644 index 00000000000..7261c8ccc40 --- /dev/null +++ b/eslint-local-rules/index.js @@ -0,0 +1,14 @@ +require("ts-node").register({ + transpileOnly: true, + compilerOptions: { + // we need this to be nodenext in the tsconfig, because + // @typescript-eslint/utils only seems to export ESM + // in TypeScript's eyes, but it totally works + module: "commonjs", + moduleResolution: "node", + }, +}); + +module.exports = { + "require-using-disposable": require("./require-using-disposable").rule, +}; diff --git a/eslint-local-rules/package.json b/eslint-local-rules/package.json new file mode 100644 index 00000000000..081ef968796 --- /dev/null +++ b/eslint-local-rules/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "test": "ts-node *.test.ts" + } +} diff --git a/eslint-local-rules/require-using-disposable.test.ts b/eslint-local-rules/require-using-disposable.test.ts new file mode 100644 index 00000000000..3e7e061cce2 --- /dev/null +++ b/eslint-local-rules/require-using-disposable.test.ts @@ -0,0 +1,31 @@ +import { rule } from "./require-using-disposable"; +import { ruleTester } from "./testSetup"; + +ruleTester.run("require-using-disposable", rule, { + valid: [ + ` + function foo(): Disposable {} + using bar = foo() + `, + ` + function foo(): AsyncDisposable {} + await using bar = foo() + `, + ], + invalid: [ + { + code: ` + function foo(): Disposable {} + const bar = foo() + `, + errors: [{ messageId: "missingUsing" }], + }, + { + code: ` + function foo(): AsyncDisposable {} + const bar = foo() + `, + errors: [{ messageId: "missingAwaitUsing" }], + }, + ], +}); diff --git a/config/eslint-rules/require-using-disposable.mjs b/eslint-local-rules/require-using-disposable.ts similarity index 88% rename from config/eslint-rules/require-using-disposable.mjs rename to eslint-local-rules/require-using-disposable.ts index 08cd20426a1..7aad6f821cb 100644 --- a/config/eslint-rules/require-using-disposable.mjs +++ b/eslint-local-rules/require-using-disposable.ts @@ -1,6 +1,5 @@ -// @ts-check import { ESLintUtils } from "@typescript-eslint/utils"; -import { unionTypeParts, isObjectType } from "ts-api-tools"; +import ts from "typescript"; export const rule = ESLintUtils.RuleCreator.withoutDocs({ create(context) { @@ -10,8 +9,9 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ if (!declarator.init) continue; const services = ESLintUtils.getParserServices(context); const type = services.getTypeAtLocation(declarator.init); - for (const typePart of unionTypeParts(type)) { - if (!isObjectType(typePart)) { + for (const typePart of type.isUnion() ? type.types : [type]) { + // is object type + if (!type || !(type.flags & ts.TypeFlags.Object)) { continue; } if ( diff --git a/eslint-local-rules/testSetup.ts b/eslint-local-rules/testSetup.ts new file mode 100644 index 00000000000..27443e815c5 --- /dev/null +++ b/eslint-local-rules/testSetup.ts @@ -0,0 +1,15 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; +import nodeTest from "node:test"; + +RuleTester.it = nodeTest.it; +RuleTester.itOnly = nodeTest.only; +RuleTester.describe = nodeTest.describe; +RuleTester.afterAll = nodeTest.after; + +export const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname + "/fixtures", + }, +}); diff --git a/config/eslint-rules/tsconfig.json b/eslint-local-rules/tsconfig.json similarity index 65% rename from config/eslint-rules/tsconfig.json rename to eslint-local-rules/tsconfig.json index 1bebd83349c..9d48812881c 100644 --- a/config/eslint-rules/tsconfig.json +++ b/eslint-local-rules/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { - "allowJs": true + "allowJs": true, + "noEmit": true, } } diff --git a/package-lock.json b/package-lock.json index 5dbbf8a653c..1fabdc08a5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "@types/use-sync-external-store": "0.0.3", "@typescript-eslint/eslint-plugin": "6.6.0", "@typescript-eslint/parser": "6.6.0", - "@typescript-eslint/rule-tester": "^6.6.0", + "@typescript-eslint/rule-tester": "6.6.0", "@typescript-eslint/types": "6.6.0", "@typescript-eslint/utils": "6.6.0", "acorn": "8.10.0", @@ -61,6 +61,8 @@ "eslint": "8.48.0", "eslint-import-resolver-typescript": "3.6.0", "eslint-plugin-import": "npm:@phryneas/eslint-plugin-import@2.27.5-pr.2813.2817.199971c", + "eslint-plugin-local-rules": "2.0.0", + "eslint-plugin-testing-library": "5.11.1", "expect-type": "0.16.0", "fetch-mock": "9.11.0", "glob": "8.1.0", @@ -4884,6 +4886,165 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-local-rules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-local-rules/-/eslint-plugin-local-rules-2.0.0.tgz", + "integrity": "sha512-sWueme0kUcP0JC1+6OBDQ9edBDVFJR92WJHSRbhiRExlenMEuUisdaVBPR+ItFBFXo2Pdw6FD2UfGZWkz8e93g==", + "dev": true + }, + "node_modules/eslint-plugin-testing-library": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", + "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.58.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -10643,6 +10804,27 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/tty-table": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.6.tgz", diff --git a/package.json b/package.json index 42e06d34ca5..b5d5bc19112 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "@types/use-sync-external-store": "0.0.3", "@typescript-eslint/eslint-plugin": "6.6.0", "@typescript-eslint/parser": "6.6.0", - "@typescript-eslint/rule-tester": "^6.6.0", + "@typescript-eslint/rule-tester": "6.6.0", "@typescript-eslint/types": "6.6.0", "@typescript-eslint/utils": "6.6.0", "acorn": "8.10.0", @@ -138,6 +138,8 @@ "eslint": "8.48.0", "eslint-import-resolver-typescript": "3.6.0", "eslint-plugin-import": "npm:@phryneas/eslint-plugin-import@2.27.5-pr.2813.2817.199971c", + "eslint-plugin-local-rules": "2.0.0", + "eslint-plugin-testing-library": "5.11.1", "expect-type": "0.16.0", "fetch-mock": "9.11.0", "glob": "8.1.0", diff --git a/tsconfig.tests.json b/tsconfig.tests.json new file mode 100644 index 00000000000..d6bb25bd3fd --- /dev/null +++ b/tsconfig.tests.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/__tests__/**/*.ts", "src/**/__tests__/**/*.tsx"], + "exclude": [] +} From 162e9da16af4875cdfe382cf48cd626a880c1c1b Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 7 Sep 2023 18:15:51 +0200 Subject: [PATCH 04/12] swap out many cases of manual console mocking --- src/__tests__/ApolloClient.ts | 107 +- src/__tests__/client.ts | 87 +- src/__tests__/local-state/export.ts | 15 +- src/__tests__/local-state/general.ts | 11 +- src/__tests__/mutationResults.ts | 12 +- src/cache/inmemory/__tests__/entityStore.ts | 29 +- src/cache/inmemory/__tests__/policies.ts | 1259 ++++++++--------- src/cache/inmemory/__tests__/roundtrip.ts | 122 +- src/cache/inmemory/__tests__/writeToStore.ts | 340 +++-- .../__tests__/useBackgroundQuery.test.tsx | 51 +- .../hooks/__tests__/useFragment.test.tsx | 21 +- .../hooks/__tests__/useMutation.test.tsx | 22 +- src/react/hooks/__tests__/useQuery.test.tsx | 10 +- src/testing/__tests__/using.test.ts | 85 -- src/testing/core/withConsoleSpy.ts | 3 + .../__tests__/spyOnConsole.test.ts | 63 + .../disposables/__tests__/withCleanup.test.ts | 13 + src/testing/internal/disposables/index.ts | 2 + .../internal/disposables/spyOnConsole.ts | 20 + .../internal/disposables/withCleanup.ts | 11 + src/testing/internal/index.ts | 1 + 21 files changed, 1125 insertions(+), 1159 deletions(-) delete mode 100644 src/testing/__tests__/using.test.ts create mode 100644 src/testing/internal/disposables/__tests__/spyOnConsole.test.ts create mode 100644 src/testing/internal/disposables/__tests__/withCleanup.test.ts create mode 100644 src/testing/internal/disposables/index.ts create mode 100644 src/testing/internal/disposables/spyOnConsole.ts create mode 100644 src/testing/internal/disposables/withCleanup.ts diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 613f5829b38..fac2ab63c57 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -14,7 +14,8 @@ import { Observable } from "../utilities"; import { ApolloLink } from "../link/core"; import { HttpLink } from "../link/http"; import { InMemoryCache } from "../cache"; -import { itAsync, withErrorSpy } from "../testing"; +import { itAsync } from "../testing"; +import { spyOnConsole } from "../testing/internal"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { invariant } from "../utilities/globals"; @@ -803,40 +804,37 @@ describe("ApolloClient", () => { }); }); - withErrorSpy( - it, - "should warn when the data provided does not match the query shape", - () => { - const client = new ApolloClient({ - link: ApolloLink.empty(), - cache: new InMemoryCache({ - // Passing an empty map enables the warning: - possibleTypes: {}, - }), - }); + it("should warn when the data provided does not match the query shape", () => { + using _consoleSpies = spyOnConsole("error"); + const client = new ApolloClient({ + link: ApolloLink.empty(), + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), + }); - client.writeQuery({ - data: { - todos: [ - { - id: "1", - name: "Todo 1", - __typename: "Todo", - }, - ], - }, - query: gql` - query { - todos { - id - name - description - } + client.writeQuery({ + data: { + todos: [ + { + id: "1", + name: "Todo 1", + __typename: "Todo", + }, + ], + }, + query: gql` + query { + todos { + id + name + description } - `, - }); - } - ); + } + `, + }); + }); }); describe("writeFragment", () => { @@ -1090,30 +1088,27 @@ describe("ApolloClient", () => { }); }); - withErrorSpy( - it, - "should warn when the data provided does not match the fragment shape", - () => { - const client = new ApolloClient({ - link: ApolloLink.empty(), - cache: new InMemoryCache({ - // Passing an empty map enables the warning: - possibleTypes: {}, - }), - }); + it("should warn when the data provided does not match the fragment shape", () => { + using _consoleSpies = spyOnConsole("error"); + const client = new ApolloClient({ + link: ApolloLink.empty(), + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), + }); - client.writeFragment({ - data: { __typename: "Bar", i: 10 }, - id: "bar", - fragment: gql` - fragment fragmentBar on Bar { - i - e - } - `, - }); - } - ); + client.writeFragment({ + data: { __typename: "Bar", i: 10 }, + id: "bar", + fragment: gql` + fragment fragmentBar on Bar { + i + e + } + `, + }); + }); describe("change will call observable next", () => { const query = gql` diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index c89098362d4..b98799cd69c 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -40,10 +40,10 @@ import { itAsync, subscribeAndCount, mockSingleLink, - withErrorSpy, MockLink, wait, } from "../testing"; +import { spyOnConsole } from "../testing/internal"; import { waitFor } from "@testing-library/react"; describe("client", () => { @@ -2879,53 +2879,50 @@ describe("client", () => { .then(resolve, reject); }); - withErrorSpy( - itAsync, - "should warn if server returns wrong data", - (resolve, reject) => { - const query = gql` - query { - todos { - id - name - description - __typename - } + itAsync("should warn if server returns wrong data", (resolve, reject) => { + using _consoleSpies = spyOnConsole("error"); + const query = gql` + query { + todos { + id + name + description + __typename } - `; - const result = { - data: { - todos: [ - { - id: "1", - name: "Todo 1", - price: 100, - __typename: "Todo", - }, - ], - }, - }; + } + `; + const result = { + data: { + todos: [ + { + id: "1", + name: "Todo 1", + price: 100, + __typename: "Todo", + }, + ], + }, + }; - const link = mockSingleLink({ - request: { query }, - result, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - // Passing an empty map enables the warning: - possibleTypes: {}, - }), - }); + const link = mockSingleLink({ + request: { query }, + result, + }).setOnError(reject); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), + }); - return client - .query({ query }) - .then(({ data }) => { - expect(data).toEqual(result.data); - }) - .then(resolve, reject); - } - ); + return client + .query({ query }) + .then(({ data }) => { + expect(data).toEqual(result.data); + }) + .then(resolve, reject); + }); itAsync( "runs a query with the connection directive and writes it to the store key defined in the directive", diff --git a/src/__tests__/local-state/export.ts b/src/__tests__/local-state/export.ts index 9b2a27dc962..616033fba3b 100644 --- a/src/__tests__/local-state/export.ts +++ b/src/__tests__/local-state/export.ts @@ -2,10 +2,11 @@ import gql from "graphql-tag"; import { print } from "graphql"; import { Observable } from "../../utilities"; -import { itAsync, withErrorSpy } from "../../testing"; +import { itAsync } from "../../testing"; import { ApolloLink } from "../../link/core"; import { ApolloClient } from "../../core"; import { InMemoryCache } from "../../cache"; +import { spyOnConsole } from "../../testing/internal"; describe("@client @export tests", () => { itAsync( @@ -179,10 +180,10 @@ describe("@client @export tests", () => { } ); - withErrorSpy( - itAsync, + itAsync( "should allow @client @export variables to be used with remote queries", (resolve, reject) => { + using _consoleSpies = spyOnConsole("error"); const query = gql` query currentAuthorPostCount($authorId: Int!) { currentAuthor @client { @@ -728,12 +729,12 @@ describe("@client @export tests", () => { } ); - withErrorSpy( - itAsync, + itAsync( "should refetch if an @export variable changes, the current fetch " + "policy is not cache-only, and the query includes fields that need to " + "be resolved remotely", (resolve, reject) => { + using _consoleSpies = spyOnConsole("error"); const query = gql` query currentAuthorPostCount($authorId: Int!) { currentAuthorId @client @export(as: "authorId") @@ -794,12 +795,12 @@ describe("@client @export tests", () => { } ); - withErrorSpy( - itAsync, + itAsync( "should NOT refetch if an @export variable has not changed, the " + "current fetch policy is not cache-only, and the query includes fields " + "that need to be resolved remotely", (resolve, reject) => { + using _consoleSpies = spyOnConsole("error"); const query = gql` query currentAuthorPostCount($authorId: Int!) { currentAuthorId @client @export(as: "authorId") diff --git a/src/__tests__/local-state/general.ts b/src/__tests__/local-state/general.ts index 4cdb0adb96a..59fe62366d2 100644 --- a/src/__tests__/local-state/general.ts +++ b/src/__tests__/local-state/general.ts @@ -17,7 +17,8 @@ import { ApolloLink } from "../../link/core"; import { Operation } from "../../link/core"; import { ApolloClient } from "../../core"; import { ApolloCache, InMemoryCache } from "../../cache"; -import { itAsync, withErrorSpy } from "../../testing"; +import { itAsync } from "../../testing"; +import { spyOnConsole } from "../../testing/internal"; describe("General functionality", () => { it("should not impact normal non-@client use", () => { @@ -1014,10 +1015,10 @@ describe("Combining client and server state/operations", () => { } ); - withErrorSpy( - itAsync, + itAsync( "should handle a simple query with both server and client fields", (resolve, reject) => { + using _consoleSpies = spyOnConsole("error"); const query = gql` query GetCount { count @client @@ -1053,10 +1054,10 @@ describe("Combining client and server state/operations", () => { } ); - withErrorSpy( - itAsync, + itAsync( "should support nested querying of both server and client fields", (resolve, reject) => { + using _consoleSpies = spyOnConsole("error"); const query = gql` query GetUser { user { diff --git a/src/__tests__/mutationResults.ts b/src/__tests__/mutationResults.ts index 115902f14c5..9452bffeebd 100644 --- a/src/__tests__/mutationResults.ts +++ b/src/__tests__/mutationResults.ts @@ -9,12 +9,8 @@ import { Observable, ObservableSubscription as Subscription, } from "../utilities"; -import { - itAsync, - subscribeAndCount, - mockSingleLink, - withErrorSpy, -} from "../testing"; +import { itAsync, subscribeAndCount, mockSingleLink } from "../testing"; +import { spyOnConsole } from "../testing/internal"; describe("mutation results", () => { const query = gql` @@ -436,10 +432,10 @@ describe("mutation results", () => { } ); - withErrorSpy( - itAsync, + itAsync( "should warn when the result fields don't match the query fields", (resolve, reject) => { + using _consoleSpies = spyOnConsole("error"); let handle: any; let subscriptionHandle: Subscription; diff --git a/src/cache/inmemory/__tests__/entityStore.ts b/src/cache/inmemory/__tests__/entityStore.ts index 00509a1fd39..72af8c300d7 100644 --- a/src/cache/inmemory/__tests__/entityStore.ts +++ b/src/cache/inmemory/__tests__/entityStore.ts @@ -15,6 +15,7 @@ import { MissingFieldError } from "../.."; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { stringifyForDisplay } from "../../../utilities"; import { InvariantError } from "../../../utilities/globals"; +import { spyOnConsole } from "../../../testing/internal"; describe("EntityStore", () => { it("should support result caching if so configured", () => { @@ -1785,23 +1786,17 @@ describe("EntityStore", () => { ).toBe('ABCs:{"b":2,"a":1,"c":3}'); { - // TODO Extact this to a helper function. - const consoleWarnSpy = jest.spyOn(console, "warn"); - consoleWarnSpy.mockImplementation(() => {}); - try { - expect(cache.identify(ABCs)).toBeUndefined(); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith( - new InvariantError( - `Missing field 'b' while extracting keyFields from ${stringifyForDisplay( - ABCs, - 2 - )}` - ) - ); - } finally { - consoleWarnSpy.mockRestore(); - } + using consoleSpies = spyOnConsole("warn"); + expect(cache.identify(ABCs)).toBeUndefined(); + expect(consoleSpies.warn).toHaveBeenCalledTimes(1); + expect(consoleSpies.warn).toHaveBeenCalledWith( + new InvariantError( + `Missing field 'b' while extracting keyFields from ${stringifyForDisplay( + ABCs, + 2 + )}` + ) + ); } expect( diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts index 1e396efd7c0..a477704f4a6 100644 --- a/src/cache/inmemory/__tests__/policies.ts +++ b/src/cache/inmemory/__tests__/policies.ts @@ -13,13 +13,8 @@ import { import { MissingFieldError } from "../.."; import { relayStylePagination, stringifyForDisplay } from "../../../utilities"; import { FieldPolicy, StorageType } from "../policies"; -import { - itAsync, - withErrorSpy, - withWarningSpy, - subscribeAndCount, - MockLink, -} from "../../../testing/core"; +import { itAsync, subscribeAndCount, MockLink } from "../../../testing/core"; +import { spyOnConsole } from "../../../testing/internal"; function reverse(s: string) { return s.split("").reverse().join(""); @@ -422,7 +417,8 @@ describe("type policies", function () { checkAuthorName(cache); }); - withErrorSpy(it, "complains about missing key fields", function () { + it("complains about missing key fields", function () { + using _consoleSpies = spyOnConsole("error"); const cache = new InMemoryCache({ typePolicies: { Book: { @@ -2719,493 +2715,486 @@ describe("type policies", function () { }); }); - withErrorSpy( - it, - "readField helper function calls custom read functions", - function () { - // Rather than writing ownTime data into the cache, we maintain it - // externally in this object: - const ownTimes: Record> = { - "parent task": makeVar(2), - "child task 1": makeVar(3), - "child task 2": makeVar(4), - "grandchild task": makeVar(5), - "independent task": makeVar(11), - }; - - const cache = new InMemoryCache({ - typePolicies: { - Agenda: { - fields: { - taskCount(_, { readField }) { - return readField("tasks")!.length; - }, - - tasks: { - // Thanks to this read function, the readField("tasks") - // call above will always return an array, so we don't - // have to guard against the possibility that the tasks - // data is undefined above. - read(existing = []) { - return existing; - }, + it("readField helper function calls custom read functions", function () { + using _consoleSpies = spyOnConsole("error"); + // Rather than writing ownTime data into the cache, we maintain it + // externally in this object: + const ownTimes: Record> = { + "parent task": makeVar(2), + "child task 1": makeVar(3), + "child task 2": makeVar(4), + "grandchild task": makeVar(5), + "independent task": makeVar(11), + }; - merge(existing: Reference[], incoming: Reference[]) { - const merged = existing ? existing.slice(0) : []; - merged.push(...incoming); - return merged; - }, - }, + const cache = new InMemoryCache({ + typePolicies: { + Agenda: { + fields: { + taskCount(_, { readField }) { + return readField("tasks")!.length; }, - }, - - Task: { - fields: { - ownTime(_, { readField }) { - const description = readField("description"); - return ownTimes[description!]() || 0; - }, - totalTime(_, { readField, toReference }) { - function total( - blockers: Readonly = [], - seen = new Set() - ) { - let time = 0; - blockers.forEach((blocker) => { - if (!seen.has(blocker.__ref)) { - seen.add(blocker.__ref); - time += readField("ownTime", blocker)!; - time += total( - readField("blockers", blocker), - seen - ); - } - }); - return time; - } - return total([ - toReference({ - __typename: "Task", - id: readField("id"), - }) as Reference, - ]); + tasks: { + // Thanks to this read function, the readField("tasks") + // call above will always return an array, so we don't + // have to guard against the possibility that the tasks + // data is undefined above. + read(existing = []) { + return existing; }, - blockers: { - merge(existing: Reference[] = [], incoming: Reference[]) { - const seenIDs = new Set(existing.map((ref) => ref.__ref)); - const merged = existing.slice(0); - incoming.forEach((ref) => { - if (!seenIDs.has(ref.__ref)) { - seenIDs.add(ref.__ref); - merged.push(ref); - } - }); - return merged; - }, + merge(existing: Reference[], incoming: Reference[]) { + const merged = existing ? existing.slice(0) : []; + merged.push(...incoming); + return merged; }, }, }, }, - }); - cache.writeQuery({ - query: gql` - query { - agenda { - id - tasks { - id - description - blockers { - id - } + Task: { + fields: { + ownTime(_, { readField }) { + const description = readField("description"); + return ownTimes[description!]() || 0; + }, + + totalTime(_, { readField, toReference }) { + function total( + blockers: Readonly = [], + seen = new Set() + ) { + let time = 0; + blockers.forEach((blocker) => { + if (!seen.has(blocker.__ref)) { + seen.add(blocker.__ref); + time += readField("ownTime", blocker)!; + time += total( + readField("blockers", blocker), + seen + ); + } + }); + return time; } - } - } - `, - data: { - agenda: { - __typename: "Agenda", - id: 1, - tasks: [ - { - __typename: "Task", - id: 1, - description: "parent task", - blockers: [ - { - __typename: "Task", - id: 2, - }, - { - __typename: "Task", - id: 3, - }, - ], - }, - { - __typename: "Task", - id: 2, - description: "child task 1", - blockers: [ - { - __typename: "Task", - id: 4, - }, - ], - }, - { - __typename: "Task", - id: 3, - description: "child task 2", - blockers: [ - { - __typename: "Task", - id: 4, - }, - ], - }, - { - __typename: "Task", - id: 4, - description: "grandchild task", + return total([ + toReference({ + __typename: "Task", + id: readField("id"), + }) as Reference, + ]); + }, + + blockers: { + merge(existing: Reference[] = [], incoming: Reference[]) { + const seenIDs = new Set(existing.map((ref) => ref.__ref)); + const merged = existing.slice(0); + incoming.forEach((ref) => { + if (!seenIDs.has(ref.__ref)) { + seenIDs.add(ref.__ref); + merged.push(ref); + } + }); + return merged; }, - ], + }, }, }, - }); - - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - agenda: { __ref: "Agenda:1" }, - }, - "Agenda:1": { - __typename: "Agenda", - id: 1, - tasks: [ - { __ref: "Task:1" }, - { __ref: "Task:2" }, - { __ref: "Task:3" }, - { __ref: "Task:4" }, - ], - }, - "Task:1": { - __typename: "Task", - blockers: [{ __ref: "Task:2" }, { __ref: "Task:3" }], - description: "parent task", - id: 1, - }, - "Task:2": { - __typename: "Task", - blockers: [{ __ref: "Task:4" }], - description: "child task 1", - id: 2, - }, - "Task:3": { - __typename: "Task", - blockers: [{ __ref: "Task:4" }], - description: "child task 2", - id: 3, - }, - "Task:4": { - __typename: "Task", - description: "grandchild task", - id: 4, - }, - }); + }, + }); - const query = gql` + cache.writeQuery({ + query: gql` query { agenda { - taskCount + id tasks { + id description - ownTime - totalTime + blockers { + id + } } } } - `; - - function read(): { agenda: any } | null { - return cache.readQuery({ query }); - } - - const firstResult = read(); - - expect(firstResult).toEqual({ + `, + data: { agenda: { __typename: "Agenda", - taskCount: 4, + id: 1, tasks: [ { __typename: "Task", + id: 1, description: "parent task", - ownTime: 2, - totalTime: 2 + 3 + 4 + 5, + blockers: [ + { + __typename: "Task", + id: 2, + }, + { + __typename: "Task", + id: 3, + }, + ], }, { __typename: "Task", + id: 2, description: "child task 1", - ownTime: 3, - totalTime: 3 + 5, + blockers: [ + { + __typename: "Task", + id: 4, + }, + ], }, { __typename: "Task", + id: 3, description: "child task 2", - ownTime: 4, - totalTime: 4 + 5, + blockers: [ + { + __typename: "Task", + id: 4, + }, + ], }, { __typename: "Task", + id: 4, description: "grandchild task", - ownTime: 5, - totalTime: 5, }, ], }, - }); + }, + }); + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + agenda: { __ref: "Agenda:1" }, + }, + "Agenda:1": { + __typename: "Agenda", + id: 1, + tasks: [ + { __ref: "Task:1" }, + { __ref: "Task:2" }, + { __ref: "Task:3" }, + { __ref: "Task:4" }, + ], + }, + "Task:1": { + __typename: "Task", + blockers: [{ __ref: "Task:2" }, { __ref: "Task:3" }], + description: "parent task", + id: 1, + }, + "Task:2": { + __typename: "Task", + blockers: [{ __ref: "Task:4" }], + description: "child task 1", + id: 2, + }, + "Task:3": { + __typename: "Task", + blockers: [{ __ref: "Task:4" }], + description: "child task 2", + id: 3, + }, + "Task:4": { + __typename: "Task", + description: "grandchild task", + id: 4, + }, + }); + + const query = gql` + query { + agenda { + taskCount + tasks { + description + ownTime + totalTime + } + } + } + `; + + function read(): { agenda: any } | null { + return cache.readQuery({ query }); + } + + const firstResult = read(); + + expect(firstResult).toEqual({ + agenda: { + __typename: "Agenda", + taskCount: 4, + tasks: [ + { + __typename: "Task", + description: "parent task", + ownTime: 2, + totalTime: 2 + 3 + 4 + 5, + }, + { + __typename: "Task", + description: "child task 1", + ownTime: 3, + totalTime: 3 + 5, + }, + { + __typename: "Task", + description: "child task 2", + ownTime: 4, + totalTime: 4 + 5, + }, + { + __typename: "Task", + description: "grandchild task", + ownTime: 5, + totalTime: 5, + }, + ], + }, + }); + + expect(read()).toBe(firstResult); + + ownTimes["child task 2"](6); + + const secondResult = read(); + expect(secondResult).not.toBe(firstResult); + expect(secondResult).toEqual({ + agenda: { + __typename: "Agenda", + taskCount: 4, + tasks: [ + { + __typename: "Task", + description: "parent task", + ownTime: 2, + totalTime: 2 + 3 + 6 + 5, + }, + { + __typename: "Task", + description: "child task 1", + ownTime: 3, + totalTime: 3 + 5, + }, + { + __typename: "Task", + description: "child task 2", + ownTime: 6, + totalTime: 6 + 5, + }, + { + __typename: "Task", + description: "grandchild task", + ownTime: 5, + totalTime: 5, + }, + ], + }, + }); + expect(secondResult!.agenda.tasks[0]).not.toBe( + firstResult!.agenda.tasks[0] + ); + expect(secondResult!.agenda.tasks[1]).toBe(firstResult!.agenda.tasks[1]); + expect(secondResult!.agenda.tasks[2]).not.toBe( + firstResult!.agenda.tasks[2] + ); + expect(secondResult!.agenda.tasks[3]).toBe(firstResult!.agenda.tasks[3]); - expect(read()).toBe(firstResult); + ownTimes["grandchild task"](7); - ownTimes["child task 2"](6); + const thirdResult = read(); + expect(thirdResult).not.toBe(secondResult); + expect(thirdResult).toEqual({ + agenda: { + __typename: "Agenda", + taskCount: 4, + tasks: [ + { + __typename: "Task", + description: "parent task", + ownTime: 2, + totalTime: 2 + 3 + 6 + 7, + }, + { + __typename: "Task", + description: "child task 1", + ownTime: 3, + totalTime: 3 + 7, + }, + { + __typename: "Task", + description: "child task 2", + ownTime: 6, + totalTime: 6 + 7, + }, + { + __typename: "Task", + description: "grandchild task", + ownTime: 7, + totalTime: 7, + }, + ], + }, + }); - const secondResult = read(); - expect(secondResult).not.toBe(firstResult); - expect(secondResult).toEqual({ + cache.writeQuery({ + query: gql` + query { + agenda { + id + tasks { + id + description + } + } + } + `, + data: { agenda: { __typename: "Agenda", - taskCount: 4, + id: 1, tasks: [ { __typename: "Task", - description: "parent task", - ownTime: 2, - totalTime: 2 + 3 + 6 + 5, - }, - { - __typename: "Task", - description: "child task 1", - ownTime: 3, - totalTime: 3 + 5, - }, - { - __typename: "Task", - description: "child task 2", - ownTime: 6, - totalTime: 6 + 5, - }, - { - __typename: "Task", - description: "grandchild task", - ownTime: 5, - totalTime: 5, + id: 5, + description: "independent task", }, ], }, - }); - expect(secondResult!.agenda.tasks[0]).not.toBe( - firstResult!.agenda.tasks[0] - ); - expect(secondResult!.agenda.tasks[1]).toBe( - firstResult!.agenda.tasks[1] - ); - expect(secondResult!.agenda.tasks[2]).not.toBe( - firstResult!.agenda.tasks[2] - ); - expect(secondResult!.agenda.tasks[3]).toBe( - firstResult!.agenda.tasks[3] - ); - - ownTimes["grandchild task"](7); - - const thirdResult = read(); - expect(thirdResult).not.toBe(secondResult); - expect(thirdResult).toEqual({ - agenda: { - __typename: "Agenda", - taskCount: 4, - tasks: [ - { - __typename: "Task", - description: "parent task", - ownTime: 2, - totalTime: 2 + 3 + 6 + 7, - }, - { - __typename: "Task", - description: "child task 1", - ownTime: 3, - totalTime: 3 + 7, - }, - { - __typename: "Task", - description: "child task 2", - ownTime: 6, - totalTime: 6 + 7, - }, - { - __typename: "Task", - description: "grandchild task", - ownTime: 7, - totalTime: 7, - }, - ], - }, - }); - - cache.writeQuery({ - query: gql` - query { - agenda { - id - tasks { - id - description - } - } - } - `, - data: { - agenda: { - __typename: "Agenda", - id: 1, - tasks: [ - { - __typename: "Task", - id: 5, - description: "independent task", - }, - ], - }, - }, - }); + }, + }); - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - agenda: { __ref: "Agenda:1" }, - }, - "Agenda:1": { - __typename: "Agenda", - id: 1, - tasks: [ - { __ref: "Task:1" }, - { __ref: "Task:2" }, - { __ref: "Task:3" }, - { __ref: "Task:4" }, - { __ref: "Task:5" }, - ], - }, - "Task:1": { - __typename: "Task", - blockers: [{ __ref: "Task:2" }, { __ref: "Task:3" }], - description: "parent task", - id: 1, - }, - "Task:2": { - __typename: "Task", - blockers: [{ __ref: "Task:4" }], - description: "child task 1", - id: 2, - }, - "Task:3": { - __typename: "Task", - blockers: [{ __ref: "Task:4" }], - description: "child task 2", - id: 3, - }, - "Task:4": { - __typename: "Task", - description: "grandchild task", - id: 4, - }, - "Task:5": { - __typename: "Task", - description: "independent task", - id: 5, - }, - }); + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + agenda: { __ref: "Agenda:1" }, + }, + "Agenda:1": { + __typename: "Agenda", + id: 1, + tasks: [ + { __ref: "Task:1" }, + { __ref: "Task:2" }, + { __ref: "Task:3" }, + { __ref: "Task:4" }, + { __ref: "Task:5" }, + ], + }, + "Task:1": { + __typename: "Task", + blockers: [{ __ref: "Task:2" }, { __ref: "Task:3" }], + description: "parent task", + id: 1, + }, + "Task:2": { + __typename: "Task", + blockers: [{ __ref: "Task:4" }], + description: "child task 1", + id: 2, + }, + "Task:3": { + __typename: "Task", + blockers: [{ __ref: "Task:4" }], + description: "child task 2", + id: 3, + }, + "Task:4": { + __typename: "Task", + description: "grandchild task", + id: 4, + }, + "Task:5": { + __typename: "Task", + description: "independent task", + id: 5, + }, + }); - const fourthResult = read(); - expect(fourthResult).not.toBe(thirdResult); - expect(fourthResult).toEqual({ - agenda: { - __typename: "Agenda", - taskCount: 5, - tasks: [ - { - __typename: "Task", - description: "parent task", - ownTime: 2, - totalTime: 2 + 3 + 6 + 7, - }, - { - __typename: "Task", - description: "child task 1", - ownTime: 3, - totalTime: 3 + 7, - }, - { - __typename: "Task", - description: "child task 2", - ownTime: 6, - totalTime: 6 + 7, - }, - { - __typename: "Task", - description: "grandchild task", - ownTime: 7, - totalTime: 7, - }, - { - __typename: "Task", - description: "independent task", - ownTime: 11, - totalTime: 11, - }, - ], - }, - }); + const fourthResult = read(); + expect(fourthResult).not.toBe(thirdResult); + expect(fourthResult).toEqual({ + agenda: { + __typename: "Agenda", + taskCount: 5, + tasks: [ + { + __typename: "Task", + description: "parent task", + ownTime: 2, + totalTime: 2 + 3 + 6 + 7, + }, + { + __typename: "Task", + description: "child task 1", + ownTime: 3, + totalTime: 3 + 7, + }, + { + __typename: "Task", + description: "child task 2", + ownTime: 6, + totalTime: 6 + 7, + }, + { + __typename: "Task", + description: "grandchild task", + ownTime: 7, + totalTime: 7, + }, + { + __typename: "Task", + description: "independent task", + ownTime: 11, + totalTime: 11, + }, + ], + }, + }); - function checkFirstFourIdentical(result: ReturnType) { - for (let i = 0; i < 4; ++i) { - expect(result!.agenda.tasks[i]).toBe(thirdResult!.agenda.tasks[i]); - } + function checkFirstFourIdentical(result: ReturnType) { + for (let i = 0; i < 4; ++i) { + expect(result!.agenda.tasks[i]).toBe(thirdResult!.agenda.tasks[i]); } - // The four original task results should not have been altered by - // the addition of a fifth independent task. - checkFirstFourIdentical(fourthResult); - - const indVar = ownTimes["independent task"]; - indVar(indVar() + 1); - - const fifthResult = read(); - expect(fifthResult).not.toBe(fourthResult); - expect(fifthResult).toEqual({ - agenda: { - __typename: "Agenda", - taskCount: 5, - tasks: [ - fourthResult!.agenda.tasks[0], - fourthResult!.agenda.tasks[1], - fourthResult!.agenda.tasks[2], - fourthResult!.agenda.tasks[3], - { - __typename: "Task", - description: "independent task", - ownTime: 12, - totalTime: 12, - }, - ], - }, - }); - checkFirstFourIdentical(fifthResult); } - ); + // The four original task results should not have been altered by + // the addition of a fifth independent task. + checkFirstFourIdentical(fourthResult); + + const indVar = ownTimes["independent task"]; + indVar(indVar() + 1); + + const fifthResult = read(); + expect(fifthResult).not.toBe(fourthResult); + expect(fifthResult).toEqual({ + agenda: { + __typename: "Agenda", + taskCount: 5, + tasks: [ + fourthResult!.agenda.tasks[0], + fourthResult!.agenda.tasks[1], + fourthResult!.agenda.tasks[2], + fourthResult!.agenda.tasks[3], + { + __typename: "Task", + description: "independent task", + ownTime: 12, + totalTime: 12, + }, + ], + }, + }); + checkFirstFourIdentical(fifthResult); + }); it("can return void to indicate missing field", function () { let secretReadAttempted = false; @@ -4368,176 +4357,173 @@ describe("type policies", function () { }); }); - withErrorSpy( - it, - "runs nested merge functions as well as ancestors", - function () { - let eventMergeCount = 0; - let attendeeMergeCount = 0; + it("runs nested merge functions as well as ancestors", function () { + using _consoleSpies = spyOnConsole("error"); + let eventMergeCount = 0; + let attendeeMergeCount = 0; - const cache = new InMemoryCache({ - typePolicies: { - Event: { - fields: { - attendees: { - merge(existing: any[], incoming: any[]) { - ++eventMergeCount; - expect(Array.isArray(incoming)).toBe(true); - return existing ? existing.concat(incoming) : incoming; - }, + const cache = new InMemoryCache({ + typePolicies: { + Event: { + fields: { + attendees: { + merge(existing: any[], incoming: any[]) { + ++eventMergeCount; + expect(Array.isArray(incoming)).toBe(true); + return existing ? existing.concat(incoming) : incoming; }, }, }, + }, - Attendee: { - fields: { - events: { - merge(existing: any[], incoming: any[]) { - ++attendeeMergeCount; - expect(Array.isArray(incoming)).toBe(true); - return existing ? existing.concat(incoming) : incoming; - }, + Attendee: { + fields: { + events: { + merge(existing: any[], incoming: any[]) { + ++attendeeMergeCount; + expect(Array.isArray(incoming)).toBe(true); + return existing ? existing.concat(incoming) : incoming; }, }, }, }, - }); + }, + }); - cache.writeQuery({ - query: gql` - query { - eventsToday { + cache.writeQuery({ + query: gql` + query { + eventsToday { + name + attendees { name - attendees { - name - events { - time - } + events { + time } } } - `, - data: { - eventsToday: [ - { - __typename: "Event", - id: 123, - name: "One-person party", - time: "noonish", - attendees: [ - { - __typename: "Attendee", - id: 234, - name: "Ben Newman", - events: [{ __typename: "Event", id: 123 }], - }, - ], - }, - ], - }, - }); + } + `, + data: { + eventsToday: [ + { + __typename: "Event", + id: 123, + name: "One-person party", + time: "noonish", + attendees: [ + { + __typename: "Attendee", + id: 234, + name: "Ben Newman", + events: [{ __typename: "Event", id: 123 }], + }, + ], + }, + ], + }, + }); - expect(eventMergeCount).toBe(1); - expect(attendeeMergeCount).toBe(1); + expect(eventMergeCount).toBe(1); + expect(attendeeMergeCount).toBe(1); - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - eventsToday: [{ __ref: "Event:123" }], - }, - "Event:123": { - __typename: "Event", - id: 123, - name: "One-person party", - attendees: [{ __ref: "Attendee:234" }], - }, - "Attendee:234": { - __typename: "Attendee", - id: 234, - name: "Ben Newman", - events: [{ __ref: "Event:123" }], - }, - }); + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + eventsToday: [{ __ref: "Event:123" }], + }, + "Event:123": { + __typename: "Event", + id: 123, + name: "One-person party", + attendees: [{ __ref: "Attendee:234" }], + }, + "Attendee:234": { + __typename: "Attendee", + id: 234, + name: "Ben Newman", + events: [{ __ref: "Event:123" }], + }, + }); - cache.writeQuery({ - query: gql` - query { - people { - name - events { - time - attendees { - name - } + cache.writeQuery({ + query: gql` + query { + people { + name + events { + time + attendees { + name } } } - `, - data: { - people: [ - { - __typename: "Attendee", - id: 234, - name: "Ben Newman", - events: [ - { - __typename: "Event", - id: 345, - name: "Rooftop dog party", - attendees: [ - { - __typename: "Attendee", - id: 456, - name: "Inspector Beckett", - }, - { - __typename: "Attendee", - id: 234, - }, - ], - }, - ], - }, - ], - }, - }); + } + `, + data: { + people: [ + { + __typename: "Attendee", + id: 234, + name: "Ben Newman", + events: [ + { + __typename: "Event", + id: 345, + name: "Rooftop dog party", + attendees: [ + { + __typename: "Attendee", + id: 456, + name: "Inspector Beckett", + }, + { + __typename: "Attendee", + id: 234, + }, + ], + }, + ], + }, + ], + }, + }); - expect(eventMergeCount).toBe(2); - expect(attendeeMergeCount).toBe(2); + expect(eventMergeCount).toBe(2); + expect(attendeeMergeCount).toBe(2); - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - eventsToday: [{ __ref: "Event:123" }], - people: [{ __ref: "Attendee:234" }], - }, - "Event:123": { - __typename: "Event", - id: 123, - name: "One-person party", - attendees: [{ __ref: "Attendee:234" }], - }, - "Event:345": { - __typename: "Event", - id: 345, - attendees: [{ __ref: "Attendee:456" }, { __ref: "Attendee:234" }], - }, - "Attendee:234": { - __typename: "Attendee", - id: 234, - name: "Ben Newman", - events: [{ __ref: "Event:123" }, { __ref: "Event:345" }], - }, - "Attendee:456": { - __typename: "Attendee", - id: 456, - name: "Inspector Beckett", - }, - }); + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + eventsToday: [{ __ref: "Event:123" }], + people: [{ __ref: "Attendee:234" }], + }, + "Event:123": { + __typename: "Event", + id: 123, + name: "One-person party", + attendees: [{ __ref: "Attendee:234" }], + }, + "Event:345": { + __typename: "Event", + id: 345, + attendees: [{ __ref: "Attendee:456" }, { __ref: "Attendee:234" }], + }, + "Attendee:234": { + __typename: "Attendee", + id: 234, + name: "Ben Newman", + events: [{ __ref: "Event:123" }, { __ref: "Event:345" }], + }, + "Attendee:456": { + __typename: "Attendee", + id: 456, + name: "Inspector Beckett", + }, + }); - expect(cache.gc()).toEqual([]); - } - ); + expect(cache.gc()).toEqual([]); + }); it("should report dangling references returned by read functions", function () { const cache = new InMemoryCache({ @@ -5963,69 +5949,66 @@ describe("type policies", function () { }); }); - withWarningSpy( - it, - "readField warns if explicitly passed undefined `from` option", - function () { - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - fullNameWithDefaults(_, { readField }) { - return `${readField({ - fieldName: "firstName", - })} ${readField("lastName")}`; - }, + it("readField warns if explicitly passed undefined `from` option", function () { + using _consoleSpies = spyOnConsole("warn"); + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + fullNameWithDefaults(_, { readField }) { + return `${readField({ + fieldName: "firstName", + })} ${readField("lastName")}`; + }, - fullNameWithVoids(_, { readField }) { - return `${readField({ - fieldName: "firstName", - // If options.from is explicitly passed but undefined, - // readField should not default to reading from the current - // object (see issue #8499). - from: void 0, - })} ${ - // Likewise for the shorthand version of readField. - readField("lastName", void 0) - }`; - }, + fullNameWithVoids(_, { readField }) { + return `${readField({ + fieldName: "firstName", + // If options.from is explicitly passed but undefined, + // readField should not default to reading from the current + // object (see issue #8499). + from: void 0, + })} ${ + // Likewise for the shorthand version of readField. + readField("lastName", void 0) + }`; }, }, }, - }); + }, + }); - const firstNameLastNameQuery = gql` - query { - firstName - lastName - } - `; + const firstNameLastNameQuery = gql` + query { + firstName + lastName + } + `; - const fullNamesQuery = gql` - query { - fullNameWithVoids - fullNameWithDefaults - } - `; + const fullNamesQuery = gql` + query { + fullNameWithVoids + fullNameWithDefaults + } + `; - cache.writeQuery({ - query: firstNameLastNameQuery, - data: { - firstName: "Alan", - lastName: "Turing", - }, - }); + cache.writeQuery({ + query: firstNameLastNameQuery, + data: { + firstName: "Alan", + lastName: "Turing", + }, + }); - expect( - cache.readQuery({ - query: fullNamesQuery, - }) - ).toEqual({ - fullNameWithDefaults: "Alan Turing", - fullNameWithVoids: "undefined undefined", - }); - } - ); + expect( + cache.readQuery({ + query: fullNamesQuery, + }) + ).toEqual({ + fullNameWithDefaults: "Alan Turing", + fullNameWithVoids: "undefined undefined", + }); + }); it("can return existing object from merge function (issue #6245)", function () { const cache = new InMemoryCache({ diff --git a/src/cache/inmemory/__tests__/roundtrip.ts b/src/cache/inmemory/__tests__/roundtrip.ts index 8ae6b259ca1..c3916a928ea 100644 --- a/src/cache/inmemory/__tests__/roundtrip.ts +++ b/src/cache/inmemory/__tests__/roundtrip.ts @@ -6,7 +6,7 @@ import { StoreReader } from "../readFromStore"; import { StoreWriter } from "../writeToStore"; import { InMemoryCache } from "../inMemoryCache"; import { writeQueryToStore, readQueryFromStore, withError } from "./helpers"; -import { withErrorSpy } from "../../../testing"; +import { spyOnConsole } from "../../../testing/internal"; function assertDeeplyFrozen(value: any, stack: any[] = []) { if (value !== null && typeof value === "object" && stack.indexOf(value) < 0) { @@ -315,39 +315,36 @@ describe("roundtrip", () => { // XXX this test is weird because it assumes the server returned an incorrect result // However, the user may have written this result with client.writeQuery. - withErrorSpy( - it, - "should throw an error on two of the same inline fragment types", - () => { - expect(() => { - storeRoundtrip( - gql` - query { - all_people { - __typename - name - ... on Jedi { - side - } - ... on Jedi { - rank - } + it("should throw an error on two of the same inline fragment types", () => { + using _consoleSpies = spyOnConsole("error"); + expect(() => { + storeRoundtrip( + gql` + query { + all_people { + __typename + name + ... on Jedi { + side + } + ... on Jedi { + rank } } - `, - { - all_people: [ - { - __typename: "Jedi", - name: "Luke Skywalker", - side: "bright", - }, - ], } - ); - }).toThrowError(/Can't find field 'rank' /); - } - ); + `, + { + all_people: [ + { + __typename: "Jedi", + name: "Luke Skywalker", + side: "bright", + }, + ], + } + ); + }).toThrowError(/Can't find field 'rank' /); + }); it("should resolve fields it can on interface with non matching inline fragments", () => { return withError(() => { @@ -459,43 +456,40 @@ describe("roundtrip", () => { }); }); - withErrorSpy( - it, - "should throw on error on two of the same spread fragment types", - () => { - expect(() => { - storeRoundtrip( - gql` - fragment jediSide on Jedi { - side - } + it("should throw on error on two of the same spread fragment types", () => { + using _consoleSpies = spyOnConsole("error"); + expect(() => { + storeRoundtrip( + gql` + fragment jediSide on Jedi { + side + } - fragment jediRank on Jedi { - rank - } + fragment jediRank on Jedi { + rank + } - query { - all_people { - __typename - name - ...jediSide - ...jediRank - } + query { + all_people { + __typename + name + ...jediSide + ...jediRank } - `, - { - all_people: [ - { - __typename: "Jedi", - name: "Luke Skywalker", - side: "bright", - }, - ], } - ); - }).toThrowError(/Can't find field 'rank' /); - } - ); + `, + { + all_people: [ + { + __typename: "Jedi", + name: "Luke Skywalker", + side: "bright", + }, + ], + } + ); + }).toThrowError(/Can't find field 'rank' /); + }); it("should resolve on @include and @skip with inline fragments", () => { storeRoundtrip( diff --git a/src/cache/inmemory/__tests__/writeToStore.ts b/src/cache/inmemory/__tests__/writeToStore.ts index 382d7c214a7..ccb437449e8 100644 --- a/src/cache/inmemory/__tests__/writeToStore.ts +++ b/src/cache/inmemory/__tests__/writeToStore.ts @@ -23,9 +23,9 @@ import { itAsync } from "../../../testing/core"; import { StoreWriter } from "../writeToStore"; import { defaultNormalizedCacheFactory, writeQueryToStore } from "./helpers"; import { InMemoryCache } from "../inMemoryCache"; -import { withErrorSpy, withWarningSpy } from "../../../testing"; import { TypedDocumentNode } from "../../../core"; import { extractFragmentContext } from "../helpers"; +import { spyOnConsole } from "../../../testing/internal"; const getIdField = ({ id }: { id: string }) => id; @@ -2045,7 +2045,8 @@ describe("writing to the store", () => { }); describe('"Cache data maybe lost..." warnings', () => { - withWarningSpy(it, "should not warn when scalar fields are updated", () => { + it("should not warn when scalar fields are updated", () => { + using consoleSpy = spyOnConsole("warn"); const cache = new InMemoryCache(); const query = gql` @@ -2099,117 +2100,108 @@ describe("writing to the store", () => { } `; - withErrorSpy( - it, - "should write the result data without validating its shape when a fragment matcher is not provided", - () => { - const result = { - todos: [ - { - id: "1", - name: "Todo 1", - }, - ], - }; + it("should write the result data without validating its shape when a fragment matcher is not provided", () => { + using consoleSpy = spyOnConsole("error"); + const result = { + todos: [ + { + id: "1", + name: "Todo 1", + }, + ], + }; - const writer = new StoreWriter( - new InMemoryCache({ - dataIdFromObject: getIdField, - }) - ); + const writer = new StoreWriter( + new InMemoryCache({ + dataIdFromObject: getIdField, + }) + ); - const newStore = writeQueryToStore({ - writer, - query, - result, - }); + const newStore = writeQueryToStore({ + writer, + query, + result, + }); - expect((newStore as any).lookup("1")).toEqual(result.todos[0]); - } - ); + expect((newStore as any).lookup("1")).toEqual(result.todos[0]); + }); - withErrorSpy( - it, - "should warn when it receives the wrong data with non-union fragments", - () => { - const result = { - todos: [ - { - id: "1", - name: "Todo 1", - }, - ], - }; + it("should warn when it receives the wrong data with non-union fragments", () => { + using consoleSpy = spyOnConsole("error"); + const result = { + todos: [ + { + id: "1", + name: "Todo 1", + }, + ], + }; - const writer = new StoreWriter( - new InMemoryCache({ - dataIdFromObject: getIdField, - possibleTypes: {}, - }) - ); + const writer = new StoreWriter( + new InMemoryCache({ + dataIdFromObject: getIdField, + possibleTypes: {}, + }) + ); - writeQueryToStore({ - writer, - query, - result, - }); - } - ); + writeQueryToStore({ + writer, + query, + result, + }); + }); - withErrorSpy( - it, - "should warn when it receives the wrong data inside a fragment", - () => { - const queryWithInterface = gql` - query { - todos { - id - name - description - ...TodoFragment - } + it("should warn when it receives the wrong data inside a fragment", () => { + using consoleSpy = spyOnConsole("error"); + const queryWithInterface = gql` + query { + todos { + id + name + description + ...TodoFragment } + } - fragment TodoFragment on Todo { - ... on ShoppingCartItem { - price - __typename - } - ... on TaskItem { - date - __typename - } + fragment TodoFragment on Todo { + ... on ShoppingCartItem { + price __typename } - `; + ... on TaskItem { + date + __typename + } + __typename + } + `; - const result = { - todos: [ - { - id: "1", - name: "Todo 1", - description: "Description 1", - __typename: "ShoppingCartItem", - }, - ], - }; + const result = { + todos: [ + { + id: "1", + name: "Todo 1", + description: "Description 1", + __typename: "ShoppingCartItem", + }, + ], + }; - const writer = new StoreWriter( - new InMemoryCache({ - dataIdFromObject: getIdField, - possibleTypes: { - Todo: ["ShoppingCartItem", "TaskItem"], - }, - }) - ); + const writer = new StoreWriter( + new InMemoryCache({ + dataIdFromObject: getIdField, + possibleTypes: { + Todo: ["ShoppingCartItem", "TaskItem"], + }, + }) + ); - writeQueryToStore({ - writer, - query: queryWithInterface, - result, - }); - } - ); + writeQueryToStore({ + writer, + query: queryWithInterface, + result, + }); + }); it("should warn if a result is missing __typename when required", () => { const result: any = { @@ -2259,7 +2251,8 @@ describe("writing to the store", () => { }); }); - withErrorSpy(it, "should not warn if a field is defered", () => { + it("should not warn if a field is defered", () => { + using consoleSpy = spyOnConsole("error"); const defered = gql` query LazyLoad { id @@ -2497,91 +2490,88 @@ describe("writing to the store", () => { }); }); - withErrorSpy( - it, - "should not keep reference when type of mixed inlined field changes to non-inlined field", - () => { - const store = defaultNormalizedCacheFactory(); + it("should not keep reference when type of mixed inlined field changes to non-inlined field", () => { + using consoleSpy = spyOnConsole("error"); + const store = defaultNormalizedCacheFactory(); - const query = gql` - query { - animals { - species { - id - name - } + const query = gql` + query { + animals { + species { + id + name } } - `; + } + `; - writeQueryToStore({ - writer, - query, - result: { - animals: [ - { - __typename: "Animal", - species: { - __typename: "Cat", - name: "cat", - }, + writeQueryToStore({ + writer, + query, + result: { + animals: [ + { + __typename: "Animal", + species: { + __typename: "Cat", + name: "cat", }, - ], - }, - store, - }); + }, + ], + }, + store, + }); - expect(store.toObject()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - animals: [ - { - __typename: "Animal", - species: { - __typename: "Cat", - name: "cat", - }, + expect(store.toObject()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + animals: [ + { + __typename: "Animal", + species: { + __typename: "Cat", + name: "cat", }, - ], - }, - }); + }, + ], + }, + }); - writeQueryToStore({ - writer, - query, - result: { - animals: [ - { - __typename: "Animal", - species: { - id: "dog-species", - __typename: "Dog", - name: "dog", - }, + writeQueryToStore({ + writer, + query, + result: { + animals: [ + { + __typename: "Animal", + species: { + id: "dog-species", + __typename: "Dog", + name: "dog", }, - ], - }, - store, - }); + }, + ], + }, + store, + }); - expect(store.toObject()).toEqual({ - "Dog__dog-species": { - id: "dog-species", - __typename: "Dog", - name: "dog", - }, - ROOT_QUERY: { - __typename: "Query", - animals: [ - { - __typename: "Animal", - species: makeReference("Dog__dog-species"), - }, - ], - }, - }); - } - ); + expect(store.toObject()).toEqual({ + "Dog__dog-species": { + id: "dog-species", + __typename: "Dog", + name: "dog", + }, + ROOT_QUERY: { + __typename: "Query", + animals: [ + { + __typename: "Animal", + species: makeReference("Dog__dog-species"), + }, + ], + }, + }); + }); it("should not merge { __ref } as StoreObject when mergeObjects used", () => { const merges: Array<{ diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 2f2f17dd39c..ef5c1ea8a00 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -53,7 +53,7 @@ import { import equal from "@wry/equality"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import { skipToken } from "../constants"; -import { profile } from "../../../testing/internal"; +import { profile, spyOnConsole } from "../../../testing/internal"; function renderIntegrationTest({ client, @@ -3314,7 +3314,7 @@ describe("useBackgroundQuery", () => { ).toBeInTheDocument(); }); it("throws errors when errors are returned after calling `refetch`", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); interface QueryData { character: { id: string; @@ -3370,8 +3370,6 @@ describe("useBackgroundQuery", () => { graphQLErrors: [new GraphQLError("Something went wrong")], }), ]); - - consoleSpy.mockRestore(); }); it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { interface QueryData { @@ -3646,17 +3644,15 @@ describe("useBackgroundQuery", () => { // Disable error message shown in the console due to an uncaught error. // TODO: need to determine why the error message is logged to the console // as an uncaught error since other tests do not require this. - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - - expect(screen.getByText("Loading")).toBeInTheDocument(); + { + using _consoleSpy = spyOnConsole("error"); - expect( - await screen.findByText("Oops couldn't fetch") - ).toBeInTheDocument(); + expect(screen.getByText("Loading")).toBeInTheDocument(); - consoleSpy.mockRestore(); + expect( + await screen.findByText("Oops couldn't fetch") + ).toBeInTheDocument(); + } const button = screen.getByText("Retry"); @@ -4825,7 +4821,7 @@ describe("useBackgroundQuery", () => { }); it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { - const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + using _consoleSpy = spyOnConsole("warn"); interface Data { character: { id: string; @@ -4952,12 +4948,10 @@ describe("useBackgroundQuery", () => { error: undefined, }, ]); - - consoleSpy.mockRestore(); }); it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { - const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + using _consoleSpy = spyOnConsole("warn"); const query: TypedDocumentNode = gql` query UserQuery { @@ -4984,8 +4978,6 @@ describe("useBackgroundQuery", () => { expect(console.warn).toHaveBeenCalledWith( "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy." ); - - consoleSpy.mockRestore(); }); it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { @@ -5212,17 +5204,18 @@ describe("useBackgroundQuery", () => { // We are intentionally writing partial data to the cache. Supress console // warnings to avoid unnecessary noise in the test. - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - }, - }); - consoleSpy.mockRestore(); + }); + } interface Renders { errors: Error[]; diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx index 9aca29b4ee2..6ef04ad4d01 100644 --- a/src/react/hooks/__tests__/useFragment.test.tsx +++ b/src/react/hooks/__tests__/useFragment.test.tsx @@ -29,7 +29,7 @@ import { concatPagination } from "../../../utilities"; import assert from "assert"; import { expectTypeOf } from "expect-type"; import { SubscriptionObserver } from "zen-observable-ts"; -import { profile } from "../../../testing/internal"; +import { profile, spyOnConsole } from "../../../testing/internal"; describe("useFragment", () => { it("is importable and callable", () => { @@ -1326,15 +1326,16 @@ describe("useFragment", () => { ); // silence the console for the incomplete fragment write - const spy = jest.spyOn(console, "error").mockImplementation(() => {}); - cache.writeFragment({ - fragment: ItemFragment, - data: { - __typename: "Item", - id: 5, - }, - }); - spy.mockRestore(); + { + using _spy = spyOnConsole("error"); + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + }, + }); + } }); it("assumes `returnPartialData: true` per default", () => { diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index 7113be78caf..7d5ffb61585 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -29,6 +29,7 @@ import { useQuery } from "../useQuery"; import { useMutation } from "../useMutation"; import { BatchHttpLink } from "../../../link/batch-http"; import { FetchResult } from "../../../link/core"; +import { spyOnConsole } from "../../../testing/internal"; describe("useMutation Hook", () => { interface Todo { @@ -227,7 +228,7 @@ describe("useMutation Hook", () => { }); it("should not call setResult on an unmounted component", async () => { - const errorSpy = jest.spyOn(console, "error"); + using consoleSpies = spyOnConsole("error"); const variables = { description: "Get milk!", }; @@ -260,8 +261,7 @@ describe("useMutation Hook", () => { await result.current.reset(); }); - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); + expect(consoleSpies.error).not.toHaveBeenCalled(); }); it("should resolve mutate function promise with mutation results", async () => { @@ -2498,7 +2498,7 @@ describe("useMutation Hook", () => { description: "Get milk!", }; it("resolves a deferred mutation with the full result", async () => { - const errorSpy = jest.spyOn(console, "error"); + using consoleSpies = spyOnConsole("error"); const link = new MockSubscriptionLink(); const client = new ApolloClient({ @@ -2579,11 +2579,10 @@ describe("useMutation Hook", () => { __typename: "Todo", }, }); - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); + expect(consoleSpies.error).not.toHaveBeenCalled(); }); it("resolves with resulting errors and calls onError callback", async () => { - const errorSpy = jest.spyOn(console, "error"); + using consoleSpies = spyOnConsole("error"); const link = new MockSubscriptionLink(); const client = new ApolloClient({ @@ -2653,12 +2652,11 @@ describe("useMutation Hook", () => { expect(onError.mock.calls[0][0].message).toBe(CREATE_TODO_ERROR); }); await waitFor(() => { - expect(errorSpy).not.toHaveBeenCalled(); + expect(consoleSpies.error).not.toHaveBeenCalled(); }); - errorSpy.mockRestore(); }); it("calls the update function with the final merged result data", async () => { - const errorSpy = jest.spyOn(console, "error"); + using consoleSpies = spyOnConsole("error"); const link = new MockSubscriptionLink(); const update = jest.fn(); const client = new ApolloClient({ @@ -2738,10 +2736,8 @@ describe("useMutation Hook", () => { expect.objectContaining({ variables }) ); await waitFor(() => { - expect(errorSpy).not.toHaveBeenCalled(); + expect(consoleSpies.error).not.toHaveBeenCalled(); }); - - errorSpy.mockRestore(); }); }); }); diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 96810b6414e..8eaa25a2cf8 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -26,7 +26,7 @@ import { import { QueryResult } from "../../types/types"; import { useQuery } from "../useQuery"; import { useMutation } from "../useMutation"; -import { profileHook } from "../../../testing/internal"; +import { profileHook, spyOnConsole } from "../../../testing/internal"; describe("useQuery Hook", () => { describe("General use", () => { @@ -3528,7 +3528,7 @@ describe("useQuery Hook", () => { it("should fetchMore with updateQuery", async () => { // TODO: Calling fetchMore with an updateQuery callback is deprecated - const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using _warnSpy = spyOnConsole("warn"); const wrapper = ({ children }: any) => ( {children} @@ -3567,13 +3567,11 @@ describe("useQuery Hook", () => { ); expect(result.current.loading).toBe(false); expect(result.current.networkStatus).toBe(NetworkStatus.ready); - - warnSpy.mockRestore(); }); it("should fetchMore with updateQuery and notifyOnNetworkStatusChange", async () => { // TODO: Calling fetchMore with an updateQuery callback is deprecated - const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using _warnSpy = spyOnConsole("warn"); const wrapper = ({ children }: any) => ( {children} @@ -3624,8 +3622,6 @@ describe("useQuery Hook", () => { ); expect(result.current.networkStatus).toBe(NetworkStatus.ready); expect(result.current.data).toEqual({ letters: ab.concat(cd) }); - - warnSpy.mockRestore(); }); it("fetchMore with concatPagination", async () => { diff --git a/src/testing/__tests__/using.test.ts b/src/testing/__tests__/using.test.ts deleted file mode 100644 index 69af8a37da4..00000000000 --- a/src/testing/__tests__/using.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -type NoInfer = [T][T extends any ? 0 : never]; - -function defineDisposable( - create: (...args: Args) => T, - cleanup: (object: NoInfer, ...args: NoInfer) => void -) { - return function (...args: Args): T & Disposable { - const obj = create(...args); - return Object.assign(obj, { - [Symbol.dispose]() { - cleanup.call(undefined, obj, args); - }, - }); - }; -} - -const mockedConsoleMethods = ["log", "info", "warn", "error", "debug"] as const; -const spyOnConsole = defineDisposable( - () => { - let originalMethods = { ...console }; - let calls: Record<(typeof mockedConsoleMethods)[number], any[][]> = - {} as any; - for (const key of mockedConsoleMethods) { - calls[key] = []; - console[key] = (...args: any[]) => { - calls[key].push(args); - }; - } - return { originalMethods, ...calls }; - }, - (object) => { - for (const key of mockedConsoleMethods) { - console[key] = object.originalMethods[key]; - } - } -); - -describe("defineDisposable", () => { - it("calls cleanup", () => { - let cleanedUp = false; - const createDisposable = defineDisposable( - () => { - return {}; - }, - () => { - cleanedUp = true; - } - ); - { - using x = createDisposable(); - } - expect(cleanedUp).toBe(true); - }); -}); - -describe("spyOnConsole", () => { - test("intercepts calls to `console.log` and `console.info`", () => { - using mockedConsole = spyOnConsole(); - console.log("hello"); - console.info("world"); - expect(mockedConsole.log).toEqual([["hello"]]); - expect(mockedConsole.info).toEqual([["world"]]); - }); - - test("restores the original `console` methods", () => { - const originalLog = console.log; - const originalWarn = console.warn; - const originalError = console.error; - const originalDebug = console.debug; - const originalInfo = console.info; - { - using _x = spyOnConsole(); - expect(console.log).not.toBe(originalLog); - expect(console.warn).not.toBe(originalWarn); - expect(console.error).not.toBe(originalError); - expect(console.debug).not.toBe(originalDebug); - expect(console.info).not.toBe(originalInfo); - } - expect(console.log).toBe(originalLog); - expect(console.warn).toBe(originalWarn); - expect(console.error).toBe(originalError); - expect(console.debug).toBe(originalDebug); - expect(console.info).toBe(originalInfo); - }); -}); diff --git a/src/testing/core/withConsoleSpy.ts b/src/testing/core/withConsoleSpy.ts index 053abd3486d..c5c425e6e33 100644 --- a/src/testing/core/withConsoleSpy.ts +++ b/src/testing/core/withConsoleSpy.ts @@ -15,6 +15,7 @@ function wrapTestFunction( }; } +/** @deprecated This method will be removed in the next major version of Apollo Client */ export function withErrorSpy( it: (...args: TArgs) => TResult, ...args: TArgs @@ -23,6 +24,7 @@ export function withErrorSpy( return it(...args); } +/** @deprecated This method will be removed in the next major version of Apollo Client */ export function withWarningSpy( it: (...args: TArgs) => TResult, ...args: TArgs @@ -31,6 +33,7 @@ export function withWarningSpy( return it(...args); } +/** @deprecated This method will be removed in the next major version of Apollo Client */ export function withLogSpy( it: (...args: TArgs) => TResult, ...args: TArgs diff --git a/src/testing/internal/disposables/__tests__/spyOnConsole.test.ts b/src/testing/internal/disposables/__tests__/spyOnConsole.test.ts new file mode 100644 index 00000000000..44a356b9ac6 --- /dev/null +++ b/src/testing/internal/disposables/__tests__/spyOnConsole.test.ts @@ -0,0 +1,63 @@ +import { spyOnConsole } from "../index.js"; + +const originalLog = console.log; +const originalWarn = console.warn; +const originalError = console.error; +const originalDebug = console.debug; +const originalInfo = console.info; + +describe("spyOnConsole", () => { + test("intercepts calls to `console.log` and `console.info`", () => { + using consoleSpies = spyOnConsole("log", "info"); + console.log("hello"); + console.info("world"); + expect(consoleSpies.log).toHaveBeenCalledWith("hello"); + expect(consoleSpies.info).toHaveBeenCalledWith("world"); + }); + + test("restores the original `console` methods", () => { + { + using _consoleSpies = spyOnConsole( + "log", + "info", + "warn", + "error", + "debug" + ); + expect(console.log).not.toBe(originalLog); + expect(console.warn).not.toBe(originalWarn); + expect(console.error).not.toBe(originalError); + expect(console.debug).not.toBe(originalDebug); + expect(console.info).not.toBe(originalInfo); + } + expect(console.log).toBe(originalLog); + expect(console.warn).toBe(originalWarn); + expect(console.error).toBe(originalError); + expect(console.debug).toBe(originalDebug); + expect(console.info).toBe(originalInfo); + }); + + test("only mocks requested methods", () => { + { + using consoleSpies = spyOnConsole("log", "warn"); + expect(consoleSpies.log).toBeDefined(); + expect(consoleSpies.warn).toBeDefined(); + // @ts-expect-error + expect(consoleSpies.error).not.toBeDefined(); + // @ts-expect-error + expect(consoleSpies.debug).not.toBeDefined(); + // @ts-expect-error + expect(consoleSpies.info).not.toBeDefined(); + expect(console.log).not.toBe(originalLog); + expect(console.warn).not.toBe(originalWarn); + expect(console.error).toBe(originalError); + expect(console.debug).toBe(originalDebug); + expect(console.info).toBe(originalInfo); + } + expect(console.log).toBe(originalLog); + expect(console.warn).toBe(originalWarn); + expect(console.error).toBe(originalError); + expect(console.debug).toBe(originalDebug); + expect(console.info).toBe(originalInfo); + }); +}); diff --git a/src/testing/internal/disposables/__tests__/withCleanup.test.ts b/src/testing/internal/disposables/__tests__/withCleanup.test.ts new file mode 100644 index 00000000000..dc7fa965754 --- /dev/null +++ b/src/testing/internal/disposables/__tests__/withCleanup.test.ts @@ -0,0 +1,13 @@ +import { withCleanup } from "../index.js"; +describe("withCleanup", () => { + it("calls cleanup", () => { + let cleanedUp = false; + { + using _x = withCleanup({}, () => { + cleanedUp = true; + }); + expect(cleanedUp).toBe(false); + } + expect(cleanedUp).toBe(true); + }); +}); diff --git a/src/testing/internal/disposables/index.ts b/src/testing/internal/disposables/index.ts new file mode 100644 index 00000000000..6d232565db4 --- /dev/null +++ b/src/testing/internal/disposables/index.ts @@ -0,0 +1,2 @@ +export { spyOnConsole } from "./spyOnConsole.js"; +export { withCleanup } from "./withCleanup.js"; diff --git a/src/testing/internal/disposables/spyOnConsole.ts b/src/testing/internal/disposables/spyOnConsole.ts new file mode 100644 index 00000000000..0e025c18275 --- /dev/null +++ b/src/testing/internal/disposables/spyOnConsole.ts @@ -0,0 +1,20 @@ +import { withCleanup } from "./withCleanup.js"; + +const noOp = () => {}; +const restore = (spy: jest.SpyInstance) => spy.mockRestore(); + +type ConsoleMethod = "log" | "info" | "warn" | "error" | "debug"; + +/** @internal */ +export function spyOnConsole(...spyOn: Keys) { + const spies = {} as Record>; + for (const key of spyOn) { + // @ts-ignore + spies[key] = jest.spyOn(console, key).mockImplementation(noOp); + } + return withCleanup(spies, (spies) => { + for (const spy of Object.values(spies) as jest.SpyInstance[]) { + restore(spy); + } + }); +} diff --git a/src/testing/internal/disposables/withCleanup.ts b/src/testing/internal/disposables/withCleanup.ts new file mode 100644 index 00000000000..1a8f518833e --- /dev/null +++ b/src/testing/internal/disposables/withCleanup.ts @@ -0,0 +1,11 @@ +/** @internal */ +export function withCleanup( + item: T, + cleanup: (item: T) => void +): T & Disposable { + return Object.assign(item, { + [Symbol.dispose]() { + cleanup(item); + }, + }); +} diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 2c76acf3c1d..4dd162e2ca9 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -1 +1,2 @@ export * from "./profile/index.js"; +export * from "./disposables/index.js"; From 1d6cd24e8c89fc7e118666d6c13c62ae1e24c469 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 7 Sep 2023 18:56:00 +0200 Subject: [PATCH 05/12] make lint rule more solid --- eslint-local-rules/fixtures/tsconfig.json | 3 ++- eslint-local-rules/package.json | 2 +- eslint-local-rules/require-using-disposable.ts | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/eslint-local-rules/fixtures/tsconfig.json b/eslint-local-rules/fixtures/tsconfig.json index c7b29c1143d..34e9fbdb577 100644 --- a/eslint-local-rules/fixtures/tsconfig.json +++ b/eslint-local-rules/fixtures/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "strict": true + "strict": true, + "target": "esnext" }, "include": ["file.ts", "react.tsx"] } diff --git a/eslint-local-rules/package.json b/eslint-local-rules/package.json index 081ef968796..4e041c609c8 100644 --- a/eslint-local-rules/package.json +++ b/eslint-local-rules/package.json @@ -1,5 +1,5 @@ { "scripts": { - "test": "ts-node *.test.ts" + "test": "node -r ts-node/register/transpile-only --no-warnings --test --watch *.test.ts" } } diff --git a/eslint-local-rules/require-using-disposable.ts b/eslint-local-rules/require-using-disposable.ts index 7aad6f821cb..e03c09117f0 100644 --- a/eslint-local-rules/require-using-disposable.ts +++ b/eslint-local-rules/require-using-disposable.ts @@ -11,7 +11,11 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ const type = services.getTypeAtLocation(declarator.init); for (const typePart of type.isUnion() ? type.types : [type]) { // is object type - if (!type || !(type.flags & ts.TypeFlags.Object)) { + if ( + !typePart || + !typePart.symbol || + !(typePart.flags & ts.TypeFlags.Object) + ) { continue; } if ( From 7f90db6c31c138bf634d5c0d7b1772498cd685ed Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 7 Sep 2023 18:57:49 +0200 Subject: [PATCH 06/12] format --- .eslintrc | 7 ++++++- config/version.js | 8 +++----- eslint-local-rules/tsconfig.json | 2 +- src/config/jest/setup.ts | 12 ++++++------ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.eslintrc b/.eslintrc index d84316f239d..dccf775414b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -57,7 +57,12 @@ } }, { - "files": ["**/__tests__/**/*.[jt]s", "**/__tests__/**/*.[jt]sx", "**/?(*.)+(test).[jt]s", "**/?(*.)+(test).[jt]sx"], + "files": [ + "**/__tests__/**/*.[jt]s", + "**/__tests__/**/*.[jt]sx", + "**/?(*.)+(test).[jt]s", + "**/?(*.)+(test).[jt]sx" + ], "extends": ["plugin:testing-library/react"], "parserOptions": { "project": "./tsconfig.tests.json" diff --git a/config/version.js b/config/version.js index de66893159c..9167f77919c 100644 --- a/config/version.js +++ b/config/version.js @@ -30,11 +30,9 @@ switch (process.argv[2]) { } case "verify": { - const { ApolloClient, InMemoryCache } = require(path.join( - distRoot, - "core", - "core.cjs" - )); + const { ApolloClient, InMemoryCache } = require( + path.join(distRoot, "core", "core.cjs") + ); // Though this may seem like overkill, verifying that ApolloClient is // constructible in Node.js is actually pretty useful, too! diff --git a/eslint-local-rules/tsconfig.json b/eslint-local-rules/tsconfig.json index 9d48812881c..a483dbea739 100644 --- a/eslint-local-rules/tsconfig.json +++ b/eslint-local-rules/tsconfig.json @@ -2,6 +2,6 @@ "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { "allowJs": true, - "noEmit": true, + "noEmit": true } } diff --git a/src/config/jest/setup.ts b/src/config/jest/setup.ts index 12565204858..f5ad519835f 100644 --- a/src/config/jest/setup.ts +++ b/src/config/jest/setup.ts @@ -18,12 +18,12 @@ function fail(reason = "fail was called in a test.") { globalThis.fail = fail; if (!Symbol.dispose) { - Object.defineProperty(Symbol, 'dispose', { - value: Symbol('dispose'), - }) + Object.defineProperty(Symbol, "dispose", { + value: Symbol("dispose"), + }); } if (!Symbol.asyncDispose) { - Object.defineProperty(Symbol, 'asyncDispose', { - value: Symbol('asyncDispose'), - }) + Object.defineProperty(Symbol, "asyncDispose", { + value: Symbol("asyncDispose"), + }); } From b7b6decefb7952da6f94aac341061dcabf605a21 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 7 Sep 2023 19:08:03 +0200 Subject: [PATCH 07/12] rename variables --- src/cache/inmemory/__tests__/writeToStore.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cache/inmemory/__tests__/writeToStore.ts b/src/cache/inmemory/__tests__/writeToStore.ts index ccb437449e8..43881849f1d 100644 --- a/src/cache/inmemory/__tests__/writeToStore.ts +++ b/src/cache/inmemory/__tests__/writeToStore.ts @@ -2046,7 +2046,7 @@ describe("writing to the store", () => { describe('"Cache data maybe lost..." warnings', () => { it("should not warn when scalar fields are updated", () => { - using consoleSpy = spyOnConsole("warn"); + using _consoleSpy = spyOnConsole("warn"); const cache = new InMemoryCache(); const query = gql` @@ -2101,7 +2101,7 @@ describe("writing to the store", () => { `; it("should write the result data without validating its shape when a fragment matcher is not provided", () => { - using consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole("error"); const result = { todos: [ { @@ -2127,7 +2127,7 @@ describe("writing to the store", () => { }); it("should warn when it receives the wrong data with non-union fragments", () => { - using consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole("error"); const result = { todos: [ { @@ -2152,7 +2152,7 @@ describe("writing to the store", () => { }); it("should warn when it receives the wrong data inside a fragment", () => { - using consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole("error"); const queryWithInterface = gql` query { todos { @@ -2252,7 +2252,7 @@ describe("writing to the store", () => { }); it("should not warn if a field is defered", () => { - using consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole("error"); const defered = gql` query LazyLoad { id @@ -2491,7 +2491,7 @@ describe("writing to the store", () => { }); it("should not keep reference when type of mixed inlined field changes to non-inlined field", () => { - using consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole("error"); const store = defaultNormalizedCacheFactory(); const query = gql` From 5f2654f6cb597a916d9b321fd323e8009ae524a1 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 8 Sep 2023 12:05:46 +0200 Subject: [PATCH 08/12] recreate export snapshots without tests bleeding into each other --- .../local-state/__snapshots__/export.ts.snap | 107 ------------------ 1 file changed, 107 deletions(-) diff --git a/src/__tests__/local-state/__snapshots__/export.ts.snap b/src/__tests__/local-state/__snapshots__/export.ts.snap index b73dd28148c..987be8f6b7c 100644 --- a/src/__tests__/local-state/__snapshots__/export.ts.snap +++ b/src/__tests__/local-state/__snapshots__/export.ts.snap @@ -3,13 +3,6 @@ exports[`@client @export tests should NOT refetch if an @export variable has not changed, the current fetch policy is not cache-only, and the query includes fields that need to be resolved remotely 1`] = ` [MockFunction] { "calls": Array [ - Array [ - "Missing field '%s' while writing result %o", - "postCount", - Object { - "currentAuthorId": 101, - }, - ], Array [ "Missing field '%s' while writing result %o", "postCount", @@ -23,10 +16,6 @@ exports[`@client @export tests should NOT refetch if an @export variable has not "type": "return", "value": undefined, }, - Object { - "type": "return", - "value": undefined, - }, ], } `; @@ -58,74 +47,6 @@ exports[`@client @export tests should allow @client @export variables to be used exports[`@client @export tests should refetch if an @export variable changes, the current fetch policy is not cache-only, and the query includes fields that need to be resolved remotely 1`] = ` [MockFunction] { "calls": Array [ - Array [ - "Missing field '%s' while writing result %o", - "postCount", - Object { - "appContainer": Object { - "__typename": "AppContainer", - "systemDetails": Object { - "__typename": "SystemDetails", - "currentAuthor": Object { - "__typename": "Author", - "authorId": 100, - "name": "John Smith", - }, - }, - }, - }, - ], - Array [ - "Missing field '%s' while writing result %o", - "title", - Object { - "__typename": "Post", - "id": 10, - "loggedInReviewerId": 100, - }, - ], - Array [ - "Missing field '%s' while writing result %o", - "reviewerDetails", - Object { - "postRequiringReview": Object { - "__typename": "Post", - "id": 10, - "loggedInReviewerId": 100, - }, - }, - ], - Array [ - "Missing field '%s' while writing result %o", - "id", - Object { - "__typename": "Post", - }, - ], - Array [ - "Missing field '%s' while writing result %o", - "title", - Object { - "__typename": "Post", - }, - ], - Array [ - "Missing field '%s' while writing result %o", - "reviewerDetails", - Object { - "postRequiringReview": Object { - "__typename": "Post", - }, - }, - ], - Array [ - "Missing field '%s' while writing result %o", - "post", - Object { - "primaryReviewerId": 100, - "secondaryReviewerId": 200, - }, - ], Array [ "Missing field '%s' while writing result %o", "postCount", @@ -139,34 +60,6 @@ exports[`@client @export tests should refetch if an @export variable changes, th "type": "return", "value": undefined, }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, ], } `; From 6ae0eab8da26aa5fe4b1491771781a4cccf28a39 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 8 Sep 2023 12:33:20 +0200 Subject: [PATCH 09/12] restore snapshotting behaviour --- .../require-using-disposable.ts | 22 +- package-lock.json | 20 +- package.json | 2 +- src/__tests__/ApolloClient.ts | 4 +- src/__tests__/client.ts | 82 +++---- .../local-state/__snapshots__/export.ts.snap | 11 + src/__tests__/local-state/export.ts | 225 +++++++++--------- src/__tests__/local-state/general.ts | 22 +- src/__tests__/mutationResults.ts | 11 +- src/cache/inmemory/__tests__/policies.ts | 8 +- src/cache/inmemory/__tests__/roundtrip.ts | 4 +- src/cache/inmemory/__tests__/writeToStore.ts | 12 +- .../internal/disposables/spyOnConsole.ts | 20 +- .../internal/disposables/withCleanup.ts | 8 +- 14 files changed, 238 insertions(+), 213 deletions(-) diff --git a/eslint-local-rules/require-using-disposable.ts b/eslint-local-rules/require-using-disposable.ts index e03c09117f0..35489c166a3 100644 --- a/eslint-local-rules/require-using-disposable.ts +++ b/eslint-local-rules/require-using-disposable.ts @@ -1,5 +1,6 @@ import { ESLintUtils } from "@typescript-eslint/utils"; import ts from "typescript"; +import * as utils from "ts-api-utils"; export const rule = ESLintUtils.RuleCreator.withoutDocs({ create(context) { @@ -9,19 +10,14 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ if (!declarator.init) continue; const services = ESLintUtils.getParserServices(context); const type = services.getTypeAtLocation(declarator.init); - for (const typePart of type.isUnion() ? type.types : [type]) { - // is object type - if ( - !typePart || - !typePart.symbol || - !(typePart.flags & ts.TypeFlags.Object) - ) { + for (const typePart of parts(type)) { + if (!utils.isObjectType(typePart) || !typePart.symbol) { continue; } if ( // bad check, but will do for now // in the future, we should check for a `[Symbol.disposable]` property - // but I have no idea how to use that right now + // but I have no idea how to do that right now typePart.symbol.escapedName === "Disposable" && node.kind != "using" ) { @@ -31,7 +27,7 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ }); } if ( - // bad check + // similarly bad check typePart.symbol.escapedName === "AsyncDisposable" && node.kind != "await using" ) { @@ -57,3 +53,11 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ }, defaultOptions: [], }); + +function parts(type: ts.Type): ts.Type[] { + return type.isUnion() + ? utils.unionTypeParts(type).flatMap(parts) + : type.isIntersection() + ? utils.intersectionTypeParts(type).flatMap(parts) + : [type]; +} diff --git a/package-lock.json b/package-lock.json index 1fabdc08a5e..9e3143d9940 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,7 +88,7 @@ "size-limit": "8.2.6", "subscriptions-transport-ws": "0.11.0", "terser": "5.19.2", - "ts-api-tools": "^0.0.20", + "ts-api-utils": "1.0.3", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", "ts-node": "10.9.1", @@ -10612,22 +10612,10 @@ "node": ">=8" } }, - "node_modules/ts-api-tools": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/ts-api-tools/-/ts-api-tools-0.0.20.tgz", - "integrity": "sha512-cUwPor7VxMQt8M2tsmfgTehNz4b03wIbJEiBUFWoSvVePNHCTuYNjWo/9jT14qeJQvouXknl81POjrR0QzMQeA==", - "dev": true, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "typescript": ">=4" - } - }, "node_modules/ts-api-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", - "integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", "dev": true, "engines": { "node": ">=16.13.0" diff --git a/package.json b/package.json index b5d5bc19112..5c5bb261b3f 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "size-limit": "8.2.6", "subscriptions-transport-ws": "0.11.0", "terser": "5.19.2", - "ts-api-tools": "^0.0.20", + "ts-api-utils": "1.0.3", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", "ts-node": "10.9.1", diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index fac2ab63c57..50c668f934c 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -805,7 +805,7 @@ describe("ApolloClient", () => { }); it("should warn when the data provided does not match the query shape", () => { - using _consoleSpies = spyOnConsole("error"); + using _consoleSpies = spyOnConsole.takeSnapshots("error"); const client = new ApolloClient({ link: ApolloLink.empty(), cache: new InMemoryCache({ @@ -1089,7 +1089,7 @@ describe("ApolloClient", () => { }); it("should warn when the data provided does not match the fragment shape", () => { - using _consoleSpies = spyOnConsole("error"); + using _consoleSpies = spyOnConsole.takeSnapshots("error"); const client = new ApolloClient({ link: ApolloLink.empty(), cache: new InMemoryCache({ diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index b98799cd69c..eb13692d37b 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -2879,49 +2879,51 @@ describe("client", () => { .then(resolve, reject); }); - itAsync("should warn if server returns wrong data", (resolve, reject) => { - using _consoleSpies = spyOnConsole("error"); - const query = gql` - query { - todos { - id - name - description - __typename + it("should warn if server returns wrong data", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + await new Promise((resolve, reject) => { + const query = gql` + query { + todos { + id + name + description + __typename + } } - } - `; - const result = { - data: { - todos: [ - { - id: "1", - name: "Todo 1", - price: 100, - __typename: "Todo", - }, - ], - }, - }; + `; + const result = { + data: { + todos: [ + { + id: "1", + name: "Todo 1", + price: 100, + __typename: "Todo", + }, + ], + }, + }; - const link = mockSingleLink({ - request: { query }, - result, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - // Passing an empty map enables the warning: - possibleTypes: {}, - }), - }); + const link = mockSingleLink({ + request: { query }, + result, + }).setOnError(reject); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), + }); - return client - .query({ query }) - .then(({ data }) => { - expect(data).toEqual(result.data); - }) - .then(resolve, reject); + return client + .query({ query }) + .then(({ data }) => { + expect(data).toEqual(result.data); + }) + .then(resolve, reject); + }); }); itAsync( diff --git a/src/__tests__/local-state/__snapshots__/export.ts.snap b/src/__tests__/local-state/__snapshots__/export.ts.snap index 987be8f6b7c..71ccfba31d1 100644 --- a/src/__tests__/local-state/__snapshots__/export.ts.snap +++ b/src/__tests__/local-state/__snapshots__/export.ts.snap @@ -54,12 +54,23 @@ exports[`@client @export tests should refetch if an @export variable changes, th "currentAuthorId": 100, }, ], + Array [ + "Missing field '%s' while writing result %o", + "postCount", + Object { + "currentAuthorId": 101, + }, + ], ], "results": Array [ Object { "type": "return", "value": undefined, }, + Object { + "type": "return", + "value": undefined, + }, ], } `; diff --git a/src/__tests__/local-state/export.ts b/src/__tests__/local-state/export.ts index 616033fba3b..ea3fb15ae5b 100644 --- a/src/__tests__/local-state/export.ts +++ b/src/__tests__/local-state/export.ts @@ -180,10 +180,9 @@ describe("@client @export tests", () => { } ); - itAsync( - "should allow @client @export variables to be used with remote queries", - (resolve, reject) => { - using _consoleSpies = spyOnConsole("error"); + it("should allow @client @export variables to be used with remote queries", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + await new Promise((resolve, reject) => { const query = gql` query currentAuthorPostCount($authorId: Int!) { currentAuthor @client { @@ -231,8 +230,8 @@ describe("@client @export tests", () => { }); resolve(); }); - } - ); + }); + }); itAsync( "should support @client @export variables that are nested multiple " + @@ -729,137 +728,141 @@ describe("@client @export tests", () => { } ); - itAsync( + it( "should refetch if an @export variable changes, the current fetch " + "policy is not cache-only, and the query includes fields that need to " + "be resolved remotely", - (resolve, reject) => { - using _consoleSpies = spyOnConsole("error"); - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; - - const testAuthorId1 = 100; - const testPostCount1 = 200; + async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + await new Promise((resolve, reject) => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; - const testAuthorId2 = 101; - const testPostCount2 = 201; + const testAuthorId1 = 100; + const testPostCount1 = 200; - let resultCount = 0; + const testAuthorId2 = 101; + const testPostCount2 = 201; - const link = new ApolloLink(() => - Observable.of({ - data: { - postCount: resultCount === 0 ? testPostCount1 : testPostCount2, - }, - }) - ); + let resultCount = 0; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const link = new ApolloLink(() => + Observable.of({ + data: { + postCount: resultCount === 0 ? testPostCount1 : testPostCount2, + }, + }) + ); + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId1 }, - }); + client.writeQuery({ + query, + data: { currentAuthorId: testAuthorId1 }, + }); - const obs = client.watchQuery({ query }); - obs.subscribe({ - next({ data }) { - if (resultCount === 0) { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId2 }, - }); - } else if (resultCount === 1) { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId2, - postCount: testPostCount2, - }); - resolve(); - } - resultCount += 1; - }, + const obs = client.watchQuery({ query }); + obs.subscribe({ + next({ data }) { + if (resultCount === 0) { + expect({ ...data }).toMatchObject({ + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }); + client.writeQuery({ + query, + data: { currentAuthorId: testAuthorId2 }, + }); + } else if (resultCount === 1) { + expect({ ...data }).toMatchObject({ + currentAuthorId: testAuthorId2, + postCount: testPostCount2, + }); + resolve(); + } + resultCount += 1; + }, + }); }); } ); - itAsync( + it( "should NOT refetch if an @export variable has not changed, the " + "current fetch policy is not cache-only, and the query includes fields " + "that need to be resolved remotely", - (resolve, reject) => { - using _consoleSpies = spyOnConsole("error"); - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; + async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + await new Promise((resolve, reject) => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; - const testAuthorId1 = 100; - const testPostCount1 = 200; + const testAuthorId1 = 100; + const testPostCount1 = 200; - const testPostCount2 = 201; + const testPostCount2 = 201; - let resultCount = 0; + let resultCount = 0; - let fetchCount = 0; - const link = new ApolloLink(() => { - fetchCount += 1; - return Observable.of({ - data: { - postCount: testPostCount1, - }, + let fetchCount = 0; + const link = new ApolloLink(() => { + fetchCount += 1; + return Observable.of({ + data: { + postCount: testPostCount1, + }, + }); }); - }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId1 }, - }); + client.writeQuery({ + query, + data: { currentAuthorId: testAuthorId1 }, + }); - const obs = client.watchQuery({ query }); - obs.subscribe({ - next(result) { - if (resultCount === 0) { - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); + const obs = client.watchQuery({ query }); + obs.subscribe({ + next(result) { + if (resultCount === 0) { + expect(fetchCount).toBe(1); + expect(result.data).toMatchObject({ + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }); - client.writeQuery({ - query, - variables: { authorId: testAuthorId1 }, - data: { postCount: testPostCount2 }, - }); - } else if (resultCount === 1) { - // Should not have refetched - expect(fetchCount).toBe(1); - resolve(); - } + client.writeQuery({ + query, + variables: { authorId: testAuthorId1 }, + data: { postCount: testPostCount2 }, + }); + } else if (resultCount === 1) { + // Should not have refetched + expect(fetchCount).toBe(1); + resolve(); + } - resultCount += 1; - }, + resultCount += 1; + }, + }); }); } ); diff --git a/src/__tests__/local-state/general.ts b/src/__tests__/local-state/general.ts index 59fe62366d2..30d31feea33 100644 --- a/src/__tests__/local-state/general.ts +++ b/src/__tests__/local-state/general.ts @@ -1015,10 +1015,9 @@ describe("Combining client and server state/operations", () => { } ); - itAsync( - "should handle a simple query with both server and client fields", - (resolve, reject) => { - using _consoleSpies = spyOnConsole("error"); + it("should handle a simple query with both server and client fields", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + await new Promise((resolve, reject) => { const query = gql` query GetCount { count @client @@ -1051,13 +1050,12 @@ describe("Combining client and server state/operations", () => { resolve(); }, }); - } - ); + }); + }); - itAsync( - "should support nested querying of both server and client fields", - (resolve, reject) => { - using _consoleSpies = spyOnConsole("error"); + it("should support nested querying of both server and client fields", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + await new Promise((resolve, reject) => { const query = gql` query GetUser { user { @@ -1117,8 +1115,8 @@ describe("Combining client and server state/operations", () => { resolve(); }, }); - } - ); + }); + }); itAsync( "should combine both server and client mutations", diff --git a/src/__tests__/mutationResults.ts b/src/__tests__/mutationResults.ts index 9452bffeebd..d7656a9232d 100644 --- a/src/__tests__/mutationResults.ts +++ b/src/__tests__/mutationResults.ts @@ -432,10 +432,9 @@ describe("mutation results", () => { } ); - itAsync( - "should warn when the result fields don't match the query fields", - (resolve, reject) => { - using _consoleSpies = spyOnConsole("error"); + it("should warn when the result fields don't match the query fields", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + await new Promise((resolve, reject) => { let handle: any; let subscriptionHandle: Subscription; @@ -528,8 +527,8 @@ describe("mutation results", () => { expect(result).toEqual(mutationTodoResult); }) .then(resolve, reject); - } - ); + }); + }); describe("InMemoryCache type/field policies", () => { const startTime = Date.now(); diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts index a477704f4a6..01beba28898 100644 --- a/src/cache/inmemory/__tests__/policies.ts +++ b/src/cache/inmemory/__tests__/policies.ts @@ -418,7 +418,7 @@ describe("type policies", function () { }); it("complains about missing key fields", function () { - using _consoleSpies = spyOnConsole("error"); + using _consoleSpies = spyOnConsole.takeSnapshots("error"); const cache = new InMemoryCache({ typePolicies: { Book: { @@ -2716,7 +2716,7 @@ describe("type policies", function () { }); it("readField helper function calls custom read functions", function () { - using _consoleSpies = spyOnConsole("error"); + using _consoleSpies = spyOnConsole.takeSnapshots("error"); // Rather than writing ownTime data into the cache, we maintain it // externally in this object: const ownTimes: Record> = { @@ -4358,7 +4358,7 @@ describe("type policies", function () { }); it("runs nested merge functions as well as ancestors", function () { - using _consoleSpies = spyOnConsole("error"); + using _consoleSpies = spyOnConsole.takeSnapshots("error"); let eventMergeCount = 0; let attendeeMergeCount = 0; @@ -5950,7 +5950,7 @@ describe("type policies", function () { }); it("readField warns if explicitly passed undefined `from` option", function () { - using _consoleSpies = spyOnConsole("warn"); + using _consoleSpies = spyOnConsole.takeSnapshots("warn"); const cache = new InMemoryCache({ typePolicies: { Query: { diff --git a/src/cache/inmemory/__tests__/roundtrip.ts b/src/cache/inmemory/__tests__/roundtrip.ts index c3916a928ea..6f04fabe5d7 100644 --- a/src/cache/inmemory/__tests__/roundtrip.ts +++ b/src/cache/inmemory/__tests__/roundtrip.ts @@ -316,7 +316,7 @@ describe("roundtrip", () => { // XXX this test is weird because it assumes the server returned an incorrect result // However, the user may have written this result with client.writeQuery. it("should throw an error on two of the same inline fragment types", () => { - using _consoleSpies = spyOnConsole("error"); + using _consoleSpies = spyOnConsole.takeSnapshots("error"); expect(() => { storeRoundtrip( gql` @@ -457,7 +457,7 @@ describe("roundtrip", () => { }); it("should throw on error on two of the same spread fragment types", () => { - using _consoleSpies = spyOnConsole("error"); + using _consoleSpies = spyOnConsole.takeSnapshots("error"); expect(() => { storeRoundtrip( gql` diff --git a/src/cache/inmemory/__tests__/writeToStore.ts b/src/cache/inmemory/__tests__/writeToStore.ts index 43881849f1d..f1911b0a3c4 100644 --- a/src/cache/inmemory/__tests__/writeToStore.ts +++ b/src/cache/inmemory/__tests__/writeToStore.ts @@ -2046,7 +2046,7 @@ describe("writing to the store", () => { describe('"Cache data maybe lost..." warnings', () => { it("should not warn when scalar fields are updated", () => { - using _consoleSpy = spyOnConsole("warn"); + using _consoleSpy = spyOnConsole.takeSnapshots("warn"); const cache = new InMemoryCache(); const query = gql` @@ -2101,7 +2101,7 @@ describe("writing to the store", () => { `; it("should write the result data without validating its shape when a fragment matcher is not provided", () => { - using _consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole.takeSnapshots("error"); const result = { todos: [ { @@ -2127,7 +2127,7 @@ describe("writing to the store", () => { }); it("should warn when it receives the wrong data with non-union fragments", () => { - using _consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole.takeSnapshots("error"); const result = { todos: [ { @@ -2152,7 +2152,7 @@ describe("writing to the store", () => { }); it("should warn when it receives the wrong data inside a fragment", () => { - using _consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole.takeSnapshots("error"); const queryWithInterface = gql` query { todos { @@ -2252,7 +2252,7 @@ describe("writing to the store", () => { }); it("should not warn if a field is defered", () => { - using _consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole.takeSnapshots("error"); const defered = gql` query LazyLoad { id @@ -2491,7 +2491,7 @@ describe("writing to the store", () => { }); it("should not keep reference when type of mixed inlined field changes to non-inlined field", () => { - using _consoleSpy = spyOnConsole("error"); + using _consoleSpy = spyOnConsole.takeSnapshots("error"); const store = defaultNormalizedCacheFactory(); const query = gql` diff --git a/src/testing/internal/disposables/spyOnConsole.ts b/src/testing/internal/disposables/spyOnConsole.ts index 0e025c18275..143de49fd7c 100644 --- a/src/testing/internal/disposables/spyOnConsole.ts +++ b/src/testing/internal/disposables/spyOnConsole.ts @@ -5,9 +5,16 @@ const restore = (spy: jest.SpyInstance) => spy.mockRestore(); type ConsoleMethod = "log" | "info" | "warn" | "error" | "debug"; +type Spies = Record< + Keys[number], + jest.SpyInstance +>; + /** @internal */ -export function spyOnConsole(...spyOn: Keys) { - const spies = {} as Record>; +export function spyOnConsole( + ...spyOn: Keys +): Spies & Disposable { + const spies = {} as Spies; for (const key of spyOn) { // @ts-ignore spies[key] = jest.spyOn(console, key).mockImplementation(noOp); @@ -18,3 +25,12 @@ export function spyOnConsole(...spyOn: Keys) { } }); } + +spyOnConsole.takeSnapshots = ( + ...spyOn: Keys +): Spies & Disposable => + withCleanup(spyOnConsole(...spyOn), (spies) => { + for (const spy of Object.values(spies) as jest.SpyInstance[]) { + expect(spy).toMatchSnapshot(); + } + }); diff --git a/src/testing/internal/disposables/withCleanup.ts b/src/testing/internal/disposables/withCleanup.ts index 1a8f518833e..38a633e28f7 100644 --- a/src/testing/internal/disposables/withCleanup.ts +++ b/src/testing/internal/disposables/withCleanup.ts @@ -3,9 +3,13 @@ export function withCleanup( item: T, cleanup: (item: T) => void ): T & Disposable { - return Object.assign(item, { + return { + ...item, [Symbol.dispose]() { cleanup(item); + if (Symbol.dispose in item) { + (item as Disposable)[Symbol.dispose](); + } }, - }); + }; } From fc7f29332a5329230383bf166a1e5cdff769059a Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 8 Sep 2023 13:13:42 +0200 Subject: [PATCH 10/12] remove accidentally commited file --- tsconfig.tests.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 tsconfig.tests.json diff --git a/tsconfig.tests.json b/tsconfig.tests.json deleted file mode 100644 index d6bb25bd3fd..00000000000 --- a/tsconfig.tests.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["src/**/__tests__/**/*.ts", "src/**/__tests__/**/*.tsx"], - "exclude": [] -} From 51cc9dae529d61c43d3613ae8811dae02b49bda1 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 8 Sep 2023 14:08:19 +0200 Subject: [PATCH 11/12] migrate more usages --- src/__tests__/graphqlSubscriptions.ts | 19 +--- .../__tests__/client/Subscription.test.tsx | 21 ++-- .../__tests__/useBackgroundQuery.test.tsx | 6 +- .../hooks/__tests__/useMutation.test.tsx | 9 +- src/react/hooks/__tests__/useQuery.test.tsx | 62 +++++------ .../hooks/__tests__/useSubscription.test.tsx | 52 ++++----- .../hooks/__tests__/useSuspenseQuery.test.tsx | 102 ++++++------------ .../ssr/__tests__/useReactiveVar.test.tsx | 19 ++-- .../react/__tests__/MockedProvider.test.tsx | 13 +-- tsconfig.tests.json | 5 + 10 files changed, 111 insertions(+), 197 deletions(-) create mode 100644 tsconfig.tests.json diff --git a/src/__tests__/graphqlSubscriptions.ts b/src/__tests__/graphqlSubscriptions.ts index 84feb733595..8b46eb01c26 100644 --- a/src/__tests__/graphqlSubscriptions.ts +++ b/src/__tests__/graphqlSubscriptions.ts @@ -6,6 +6,7 @@ import { ApolloError, PROTOCOL_ERRORS_SYMBOL } from "../errors"; import { QueryManager } from "../core/QueryManager"; import { itAsync, mockObservableLink } from "../testing"; import { GraphQLError } from "graphql"; +import { spyOnConsole } from "../testing/internal"; describe("GraphQL Subscriptions", () => { const results = [ @@ -328,9 +329,7 @@ describe("GraphQL Subscriptions", () => { }; // Silence expected warning about missing field for cache write - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); + using _consoleSpy = spyOnConsole("warn"); link.simulateResult(errorResult, true); @@ -348,8 +347,6 @@ describe("GraphQL Subscriptions", () => { ], }) ); - - consoleSpy.mockRestore(); }); it('strips errors in next result when `errorPolicy` is "ignore"', async () => { @@ -443,9 +440,7 @@ describe("GraphQL Subscriptions", () => { }; // Silence expected warning about missing field for cache write - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); + using _consoleSpy = spyOnConsole("warn"); link.simulateResult(errorResult, true); @@ -463,8 +458,6 @@ describe("GraphQL Subscriptions", () => { ], }) ); - - consoleSpy.mockRestore(); }); it("should call complete handler when the subscription completes", () => { @@ -546,14 +539,10 @@ describe("GraphQL Subscriptions", () => { }; // Silence expected warning about missing field for cache write - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); + using _consoleSpy = spyOnConsole("warn"); link.simulateResult(errorResult); await promise; - - consoleSpy.mockRestore(); }); }); diff --git a/src/react/components/__tests__/client/Subscription.test.tsx b/src/react/components/__tests__/client/Subscription.test.tsx index f11e262c2db..4584913a30d 100644 --- a/src/react/components/__tests__/client/Subscription.test.tsx +++ b/src/react/components/__tests__/client/Subscription.test.tsx @@ -8,6 +8,7 @@ import { ApolloProvider } from "../../../context"; import { ApolloLink, Operation } from "../../../../link/core"; import { itAsync, MockSubscriptionLink } from "../../../../testing"; import { Subscription } from "../../Subscription"; +import { spyOnConsole } from "../../../../testing/internal"; const results = [ "Luke Skywalker", @@ -118,9 +119,7 @@ it("calls onData if given", async () => { }); it("calls onSubscriptionData with deprecation warning if given", async () => { - const consoleWarnSpy = jest - .spyOn(console, "warn") - .mockImplementation(() => {}); + using consoleSpy = spyOnConsole("warn"); let count = 0; const Component = () => ( @@ -141,8 +140,8 @@ it("calls onSubscriptionData with deprecation warning if given", async () => { ); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith( + expect(consoleSpy.warn).toHaveBeenCalledTimes(1); + expect(consoleSpy.warn).toHaveBeenCalledWith( expect.stringContaining("'onSubscriptionData' is deprecated") ); @@ -152,8 +151,6 @@ it("calls onSubscriptionData with deprecation warning if given", async () => { }, 10); await waitFor(() => expect(count).toBe(4)); - - consoleWarnSpy.mockRestore(); }); it("should call onComplete if specified", async () => { @@ -187,9 +184,7 @@ it("should call onComplete if specified", async () => { }); it("should call onSubscriptionComplete with deprecation warning if specified", async () => { - const consoleWarnSpy = jest - .spyOn(console, "warn") - .mockImplementation(() => {}); + using consoleSpy = spyOnConsole("warn"); let count = 0; let done = false; @@ -211,8 +206,8 @@ it("should call onSubscriptionComplete with deprecation warning if specified", a ); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith( + expect(consoleSpy.warn).toHaveBeenCalledTimes(1); + expect(consoleSpy.warn).toHaveBeenCalledWith( expect.stringContaining("'onSubscriptionComplete' is deprecated") ); @@ -222,8 +217,6 @@ it("should call onSubscriptionComplete with deprecation warning if specified", a }, 10); await waitFor(() => expect(done).toBeTruthy()); - - consoleWarnSpy.mockRestore(); }); itAsync( diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index ef5c1ea8a00..08121af7f27 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -3767,9 +3767,7 @@ describe("useBackgroundQuery", () => { // Disable error message shown in the console due to an uncaught error. // TODO: need to determine why the error message is logged to the console // as an uncaught error since other tests do not require this. - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); + using _consoleSpy = spyOnConsole("error"); expect(screen.getByText("Loading")).toBeInTheDocument(); @@ -3790,8 +3788,6 @@ describe("useBackgroundQuery", () => { }); expect(screen.queryByText("Loading")).not.toBeInTheDocument(); - - consoleSpy.mockRestore(); }); it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index 7d5ffb61585..3ba88663d85 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -496,9 +496,7 @@ describe("useMutation Hook", () => { }); it(`should ignore errors when errorPolicy is 'ignore'`, async () => { - const errorMock = jest - .spyOn(console, "error") - .mockImplementation(() => {}); + using consoleSpy = spyOnConsole("error"); const variables = { description: "Get milk!", }; @@ -531,9 +529,8 @@ describe("useMutation Hook", () => { }); expect(fetchResult).toEqual({}); - expect(errorMock).toHaveBeenCalledTimes(1); - expect(errorMock.mock.calls[0][0]).toMatch("Missing field"); - errorMock.mockRestore(); + expect(consoleSpy.error).toHaveBeenCalledTimes(1); + expect(consoleSpy.error.mock.calls[0][0]).toMatch("Missing field"); }); it(`should not call onError when errorPolicy is 'ignore'`, async () => { diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 8eaa25a2cf8..d900a2d53fe 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -4652,7 +4652,7 @@ describe("useQuery Hook", () => { // This test was added for issue https://github.com/apollographql/apollo-client/issues/9794 it("onCompleted can set state without causing react errors", async () => { - const errorSpy = jest.spyOn(console, "error"); + using consoleSpy = spyOnConsole("error"); const query = gql` { hello @@ -4692,8 +4692,7 @@ describe("useQuery Hook", () => { render(); await screen.findByText("onCompletedCalled: true"); - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); + expect(consoleSpy.error).not.toHaveBeenCalled(); }); it("onCompleted should not execute on cache writes after initial query execution", async () => { @@ -4978,9 +4977,7 @@ describe("useQuery Hook", () => { describe("Partial refetch", () => { it("should attempt a refetch when data is missing and partialRefetch is true", async () => { - const errorSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); + using consoleSpy = spyOnConsole("error"); const query = gql` { hello @@ -5033,9 +5030,8 @@ describe("useQuery Hook", () => { expect(result.current.data).toBe(undefined); expect(result.current.error).toBe(undefined); - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy.mock.calls[0][0]).toMatch("Missing field"); - errorSpy.mockRestore(); + expect(consoleSpy.error).toHaveBeenCalledTimes(1); + expect(consoleSpy.error.mock.calls[0][0]).toMatch("Missing field"); await waitFor( () => { @@ -5064,9 +5060,7 @@ describe("useQuery Hook", () => { allPeople: { people: [{ name: "Luke Skywalker" }] }, }; - const errorSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); + using consoleSpy = spyOnConsole("error"); const link = mockSingleLink( { request: { query }, result: { data: {} }, delay: 20 }, { request: { query }, result: { data }, delay: 20 } @@ -5105,9 +5099,8 @@ describe("useQuery Hook", () => { expect(result.current.data).toBe(undefined); expect(result.current.error).toBe(undefined); - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy.mock.calls[0][0]).toMatch("Missing field"); - errorSpy.mockRestore(); + expect(consoleSpy.error).toHaveBeenCalledTimes(1); + expect(consoleSpy.error.mock.calls[0][0]).toMatch("Missing field"); await waitFor( () => { @@ -5121,9 +5114,7 @@ describe("useQuery Hook", () => { }); it("should attempt a refetch when data is missing, partialRefetch is true and addTypename is false for the cache", async () => { - const errorSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); + using consoleSpy = spyOnConsole("error"); const query = gql` { hello @@ -5177,9 +5168,8 @@ describe("useQuery Hook", () => { expect(result.current.error).toBe(undefined); expect(result.current.data).toBe(undefined); - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy.mock.calls[0][0]).toMatch("Missing field"); - errorSpy.mockRestore(); + expect(consoleSpy.error).toHaveBeenCalledTimes(1); + expect(consoleSpy.error.mock.calls[0][0]).toMatch("Missing field"); await waitFor( () => { @@ -5646,9 +5636,7 @@ describe("useQuery Hook", () => { describe("Missing Fields", () => { it("should log debug messages about MissingFieldErrors from the cache", async () => { - const errorSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); + using consoleSpy = spyOnConsole("error"); const carQuery: DocumentNode = gql` query cars($id: Int) { @@ -5705,8 +5693,8 @@ describe("useQuery Hook", () => { expect(result.current.data).toEqual(carData); expect(result.current.error).toBeUndefined(); - expect(errorSpy).toHaveBeenCalled(); - expect(errorSpy).toHaveBeenLastCalledWith( + expect(consoleSpy.error).toHaveBeenCalled(); + expect(consoleSpy.error).toHaveBeenLastCalledWith( `Missing field '%s' while writing result %o`, "vin", { @@ -5717,7 +5705,6 @@ describe("useQuery Hook", () => { __typename: "Car", } ); - errorSpy.mockRestore(); }); it("should return partial cache data when `returnPartialData` is true", async () => { @@ -8039,17 +8026,18 @@ describe("useQuery Hook", () => { // We know we are writing partial data to the cache so suppress the console // warning. - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - }, - }); - consoleSpy.mockRestore(); + }); + } const { result } = renderHook( () => diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index 2ef09995500..decdd17b973 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -14,6 +14,7 @@ import { InMemoryCache as Cache } from "../../../cache"; import { ApolloProvider } from "../../context"; import { MockSubscriptionLink } from "../../../testing"; import { useSubscription } from "../useSubscription"; +import { spyOnConsole } from "../../../testing/internal"; describe("useSubscription Hook", () => { it("should handle a simple subscription properly", async () => { @@ -525,7 +526,7 @@ describe("useSubscription Hook", () => { }); it("should handle immediate completions gracefully", async () => { - const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + using consoleSpy = spyOnConsole("error"); const subscription = gql` subscription { @@ -564,17 +565,16 @@ describe("useSubscription Hook", () => { expect(result.current.error).toBe(undefined); expect(result.current.data).toBe(null); - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy.mock.calls[0]).toStrictEqual([ + expect(consoleSpy.error).toHaveBeenCalledTimes(1); + expect(consoleSpy.error.mock.calls[0]).toStrictEqual([ "Missing field '%s' while writing result %o", "car", Object.create(null), ]); - errorSpy.mockRestore(); }); it("should handle immediate completions with multiple subscriptions gracefully", async () => { - const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + using consoleSpy = spyOnConsole("error"); const subscription = gql` subscription { car { @@ -633,27 +633,26 @@ describe("useSubscription Hook", () => { expect(result.current.sub3.error).toBe(undefined); expect(result.current.sub3.data).toBe(null); - expect(errorSpy).toHaveBeenCalledTimes(3); - expect(errorSpy.mock.calls[0]).toStrictEqual([ + expect(consoleSpy.error).toHaveBeenCalledTimes(3); + expect(consoleSpy.error.mock.calls[0]).toStrictEqual([ "Missing field '%s' while writing result %o", "car", Object.create(null), ]); - expect(errorSpy.mock.calls[1]).toStrictEqual([ + expect(consoleSpy.error.mock.calls[1]).toStrictEqual([ "Missing field '%s' while writing result %o", "car", Object.create(null), ]); - expect(errorSpy.mock.calls[2]).toStrictEqual([ + expect(consoleSpy.error.mock.calls[2]).toStrictEqual([ "Missing field '%s' while writing result %o", "car", Object.create(null), ]); - errorSpy.mockRestore(); }); test("should warn when using 'onSubscriptionData' and 'onData' together", () => { - const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using consoleSpy = spyOnConsole("warn"); const subscription = gql` subscription { car { @@ -681,13 +680,12 @@ describe("useSubscription Hook", () => { } ); - expect(warningSpy).toHaveBeenCalledTimes(1); - expect(warningSpy).toHaveBeenCalledWith( + expect(consoleSpy.warn).toHaveBeenCalledTimes(1); + expect(consoleSpy.warn).toHaveBeenCalledWith( expect.stringContaining( "supports only the 'onSubscriptionData' or 'onData' option" ) ); - warningSpy.mockRestore(); }); test("prefers 'onData' when using 'onSubscriptionData' and 'onData' together", async () => { @@ -739,7 +737,7 @@ describe("useSubscription Hook", () => { }); test("uses 'onSubscriptionData' when 'onData' is absent", async () => { - const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using _consoleSpy = spyOnConsole("warn"); const subscription = gql` subscription { car { @@ -781,11 +779,10 @@ describe("useSubscription Hook", () => { }, { interval: 1 } ); - warningSpy.mockRestore(); }); test("only warns once using `onSubscriptionData`", () => { - const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using consoleSpy = spyOnConsole("warn"); const subscription = gql` subscription { car { @@ -814,12 +811,11 @@ describe("useSubscription Hook", () => { rerender(); - expect(warningSpy).toHaveBeenCalledTimes(1); - warningSpy.mockRestore(); + expect(consoleSpy.warn).toHaveBeenCalledTimes(1); }); test("should warn when using 'onComplete' and 'onSubscriptionComplete' together", () => { - const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using consoleSpy = spyOnConsole("warn"); const subscription = gql` subscription { car { @@ -847,17 +843,16 @@ describe("useSubscription Hook", () => { } ); - expect(warningSpy).toHaveBeenCalledTimes(1); - expect(warningSpy).toHaveBeenCalledWith( + expect(consoleSpy.warn).toHaveBeenCalledTimes(1); + expect(consoleSpy.warn).toHaveBeenCalledWith( expect.stringContaining( "supports only the 'onSubscriptionComplete' or 'onComplete' option" ) ); - warningSpy.mockRestore(); }); test("prefers 'onComplete' when using 'onComplete' and 'onSubscriptionComplete' together", async () => { - const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using _consoleSpy = spyOnConsole("warn"); const subscription = gql` subscription { car { @@ -904,11 +899,10 @@ describe("useSubscription Hook", () => { { interval: 1 } ); expect(onSubscriptionComplete).toHaveBeenCalledTimes(0); - warningSpy.mockRestore(); }); test("uses 'onSubscriptionComplete' when 'onComplete' is absent", async () => { - const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using _consoleSpy = spyOnConsole("warn"); const subscription = gql` subscription { car { @@ -952,11 +946,10 @@ describe("useSubscription Hook", () => { }, { interval: 1 } ); - warningSpy.mockRestore(); }); test("only warns once using `onSubscriptionComplete`", () => { - const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using consoleSpy = spyOnConsole("warn"); const subscription = gql` subscription { car { @@ -985,8 +978,7 @@ describe("useSubscription Hook", () => { rerender(); - expect(warningSpy).toHaveBeenCalledTimes(1); - warningSpy.mockRestore(); + expect(consoleSpy.warn).toHaveBeenCalledTimes(1); }); describe("multipart subscriptions", () => { diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 9ec76d4b07c..08b4abb3f64 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -51,7 +51,7 @@ import { RefetchWritePolicy, WatchQueryFetchPolicy, } from "../../../core/watchQueryOptions"; -import { profile } from "../../../testing/internal"; +import { profile, spyOnConsole } from "../../../testing/internal"; type RenderSuspenseHookOptions = Omit< RenderHookOptions, @@ -280,7 +280,7 @@ function wait(delay: number) { describe("useSuspenseQuery", () => { it("validates the GraphQL query as a query", () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const query = gql` mutation ShouldThrow { @@ -297,13 +297,11 @@ describe("useSuspenseQuery", () => { "Running a Query requires a graphql Query, but a Mutation was used instead." ) ); - - consoleSpy.mockRestore(); }); it("ensures a valid fetch policy is used", () => { const INVALID_FETCH_POLICIES = ["cache-only", "standby"]; - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const { query } = useSimpleQueryCase(); INVALID_FETCH_POLICIES.forEach((fetchPolicy: any) => { @@ -319,8 +317,6 @@ describe("useSuspenseQuery", () => { ) ); }); - - consoleSpy.mockRestore(); }); it("ensures a valid fetch policy is used when defined via global options", () => { @@ -328,7 +324,7 @@ describe("useSuspenseQuery", () => { "cache-only", "standby", ]; - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const { query } = useSimpleQueryCase(); INVALID_FETCH_POLICIES.forEach((fetchPolicy) => { @@ -354,8 +350,6 @@ describe("useSuspenseQuery", () => { ) ); }); - - consoleSpy.mockRestore(); }); it("suspends a query and returns results", async () => { @@ -2480,7 +2474,7 @@ describe("useSuspenseQuery", () => { }); it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { - const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + using _consoleSpy = spyOnConsole("warn"); const fullQuery = gql` query { @@ -2541,12 +2535,10 @@ describe("useSuspenseQuery", () => { error: undefined, }, ]); - - consoleSpy.mockRestore(); }); it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { - const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + using consoleSpy = spyOnConsole("warn"); const { query, mocks } = useSimpleQueryCase(); @@ -2559,12 +2551,10 @@ describe("useSuspenseQuery", () => { { mocks } ); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith( + expect(consoleSpy.warn).toHaveBeenCalledTimes(1); + expect(consoleSpy.warn).toHaveBeenCalledWith( "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy." ); - - consoleSpy.mockRestore(); }); it('does not suspend when data is in the cache and using a "cache-and-network" fetch policy', async () => { @@ -3481,7 +3471,7 @@ describe("useSuspenseQuery", () => { }); it("throws network errors by default", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const { query, mocks } = useErrorCase({ networkError: new Error("Could not fetch"), @@ -3502,12 +3492,10 @@ describe("useSuspenseQuery", () => { expect(error).toBeInstanceOf(ApolloError); expect(error.networkError).toEqual(new Error("Could not fetch")); expect(error.graphQLErrors).toEqual([]); - - consoleSpy.mockRestore(); }); it("throws graphql errors by default", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const { query, mocks } = useErrorCase({ graphQLErrors: [new GraphQLError("`id` should not be null")], @@ -3530,13 +3518,11 @@ describe("useSuspenseQuery", () => { expect(error.graphQLErrors).toEqual([ new GraphQLError("`id` should not be null"), ]); - - consoleSpy.mockRestore(); }); it("tears down subscription when throwing an error", async () => { jest.useFakeTimers(); - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const { query, mocks } = useErrorCase({ networkError: new Error("Could not fetch"), @@ -3561,11 +3547,10 @@ describe("useSuspenseQuery", () => { expect(client.getObservableQueries().size).toBe(0); jest.useRealTimers(); - consoleSpy.mockRestore(); }); it("tears down subscription when throwing an error on refetch", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const query = gql` query UserQuery($id: String!) { @@ -3616,12 +3601,10 @@ describe("useSuspenseQuery", () => { await waitFor(() => expect(renders.errorCount).toBe(1)); expect(client.getObservableQueries().size).toBe(0); - - consoleSpy.mockRestore(); }); it('throws network errors when errorPolicy is set to "none"', async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const { query, mocks } = useErrorCase({ networkError: new Error("Could not fetch"), @@ -3643,12 +3626,10 @@ describe("useSuspenseQuery", () => { expect(error).toBeInstanceOf(ApolloError); expect(error.networkError).toEqual(new Error("Could not fetch")); expect(error.graphQLErrors).toEqual([]); - - consoleSpy.mockRestore(); }); it('throws graphql errors when errorPolicy is set to "none"', async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const { query, mocks } = useErrorCase({ graphQLErrors: [new GraphQLError("`id` should not be null")], @@ -3672,12 +3653,10 @@ describe("useSuspenseQuery", () => { expect(error.graphQLErrors).toEqual([ new GraphQLError("`id` should not be null"), ]); - - consoleSpy.mockRestore(); }); it('handles multiple graphql errors when errorPolicy is set to "none"', async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const graphQLErrors = [ new GraphQLError("Fool me once"), @@ -3702,12 +3681,10 @@ describe("useSuspenseQuery", () => { expect(error).toBeInstanceOf(ApolloError); expect(error!.networkError).toBeNull(); expect(error!.graphQLErrors).toEqual(graphQLErrors); - - consoleSpy.mockRestore(); }); it('throws network errors when errorPolicy is set to "ignore"', async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const networkError = new Error("Could not fetch"); const { query, mocks } = useErrorCase({ networkError }); @@ -3730,8 +3707,6 @@ describe("useSuspenseQuery", () => { expect(error).toBeInstanceOf(ApolloError); expect(error!.networkError).toEqual(networkError); expect(error!.graphQLErrors).toEqual([]); - - consoleSpy.mockRestore(); }); it('does not throw or return graphql errors when errorPolicy is set to "ignore"', async () => { @@ -3877,7 +3852,7 @@ describe("useSuspenseQuery", () => { }); it('throws network errors when errorPolicy is set to "all"', async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const networkError = new Error("Could not fetch"); @@ -3901,8 +3876,6 @@ describe("useSuspenseQuery", () => { expect(error).toBeInstanceOf(ApolloError); expect(error!.networkError).toEqual(networkError); expect(error!.graphQLErrors).toEqual([]); - - consoleSpy.mockRestore(); }); it('does not throw and returns graphql errors when errorPolicy is set to "all"', async () => { @@ -4486,7 +4459,7 @@ describe("useSuspenseQuery", () => { }); it("throws errors when errors are returned after calling `refetch`", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const query = gql` query UserQuery($id: String!) { @@ -4545,8 +4518,6 @@ describe("useSuspenseQuery", () => { error: undefined, }, ]); - - consoleSpy.mockRestore(); }); it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { @@ -6938,17 +6909,18 @@ describe("useSuspenseQuery", () => { // We are intentionally writing partial data to the cache. Supress console // warnings to avoid unnecessary noise in the test. - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - }, - }); - consoleSpy.mockRestore(); + }); + } const { result, renders } = renderSuspenseHook( () => @@ -8406,7 +8378,7 @@ describe("useSuspenseQuery", () => { ); it("throws network errors returned by deferred queries", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const query = gql` query { @@ -8442,12 +8414,10 @@ describe("useSuspenseQuery", () => { expect(error).toBeInstanceOf(ApolloError); expect(error.networkError).toEqual(new Error("Could not fetch")); expect(error.graphQLErrors).toEqual([]); - - consoleSpy.mockRestore(); }); it("throws graphql errors returned by deferred queries", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const query = gql` query { @@ -8487,12 +8457,10 @@ describe("useSuspenseQuery", () => { expect(error.graphQLErrors).toEqual([ new GraphQLError("Could not fetch greeting"), ]); - - consoleSpy.mockRestore(); }); it("throws errors returned by deferred queries that include partial data", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const query = gql` query { @@ -8533,12 +8501,10 @@ describe("useSuspenseQuery", () => { expect(error.graphQLErrors).toEqual([ new GraphQLError("Could not fetch greeting"), ]); - - consoleSpy.mockRestore(); }); it("discards partial data and throws errors returned in incremental chunks", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + using _consoleSpy = spyOnConsole("error"); const query = gql` query { @@ -8670,8 +8636,6 @@ describe("useSuspenseQuery", () => { { path: ["hero", "heroFriends", 0, "homeWorld"] } ), ]); - - consoleSpy.mockRestore(); }); it("adds partial data and does not throw errors returned in incremental chunks but returns them in `error` property with errorPolicy set to `all`", async () => { diff --git a/src/react/ssr/__tests__/useReactiveVar.test.tsx b/src/react/ssr/__tests__/useReactiveVar.test.tsx index 34a74026915..bbdb1d58875 100644 --- a/src/react/ssr/__tests__/useReactiveVar.test.tsx +++ b/src/react/ssr/__tests__/useReactiveVar.test.tsx @@ -2,12 +2,12 @@ import React from "react"; import { makeVar } from "../../../core"; import { useReactiveVar } from "../../hooks"; -import { itAsync } from "../../../testing"; import { renderToStringWithData } from "../"; +import { spyOnConsole } from "../../../testing/internal"; describe("useReactiveVar Hook SSR", () => { - itAsync("does not cause warnings", (resolve, reject) => { - const mock = jest.spyOn(console, "error"); + it("does not cause warnings", async () => { + using consoleSpy = spyOnConsole("error"); const counterVar = makeVar(0); function Component() { const count = useReactiveVar(counterVar); @@ -16,14 +16,9 @@ describe("useReactiveVar Hook SSR", () => { return
{count}
; } - renderToStringWithData() - .then((value) => { - expect(value).toEqual("
0
"); - expect(mock).toHaveBeenCalledTimes(0); - }) - .finally(() => { - mock.mockRestore(); - }) - .then(resolve, reject); + // eslint-disable-next-line testing-library/render-result-naming-convention + const value = await renderToStringWithData(); + expect(value).toEqual("
0
"); + expect(consoleSpy.error).toHaveBeenCalledTimes(0); }); }); diff --git a/src/testing/react/__tests__/MockedProvider.test.tsx b/src/testing/react/__tests__/MockedProvider.test.tsx index f3d5c43bcd3..8dd2b3be043 100644 --- a/src/testing/react/__tests__/MockedProvider.test.tsx +++ b/src/testing/react/__tests__/MockedProvider.test.tsx @@ -8,6 +8,7 @@ import { MockedProvider } from "../MockedProvider"; import { useQuery } from "../../../react/hooks"; import { InMemoryCache } from "../../../cache"; import { ApolloLink } from "../../../link/core"; +import { spyOnConsole } from "../../internal"; const variables = { username: "mock_username", @@ -521,7 +522,7 @@ describe("General use", () => { }); it("shows a warning in the console when there is no matched mock", async () => { - const consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + using _consoleSpy = spyOnConsole("warn"); let finished = false; function Component({ ...variables }: Variables) { const { loading } = useQuery(query, { variables }); @@ -561,12 +562,10 @@ describe("General use", () => { expect(console.warn).toHaveBeenCalledWith( expect.stringContaining("No more mocked responses for the query") ); - - consoleSpy.mockRestore(); }); it("silences console warning for unmatched mocks when `showWarnings` is `false`", async () => { - const consoleSpy = jest.spyOn(console, "warn"); + using _consoleSpy = spyOnConsole("warn"); let finished = false; function Component({ ...variables }: Variables) { const { loading } = useQuery(query, { variables }); @@ -603,12 +602,10 @@ describe("General use", () => { }); expect(console.warn).not.toHaveBeenCalled(); - - consoleSpy.mockRestore(); }); it("silences console warning for unmatched mocks when passing `showWarnings` to `MockLink` directly", async () => { - const consoleSpy = jest.spyOn(console, "warn"); + using _consoleSpy = spyOnConsole("warn"); let finished = false; function Component({ ...variables }: Variables) { const { loading } = useQuery(query, { variables }); @@ -649,8 +646,6 @@ describe("General use", () => { }); expect(console.warn).not.toHaveBeenCalled(); - - consoleSpy.mockRestore(); }); itAsync( diff --git a/tsconfig.tests.json b/tsconfig.tests.json new file mode 100644 index 00000000000..d6bb25bd3fd --- /dev/null +++ b/tsconfig.tests.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/__tests__/**/*.ts", "src/**/__tests__/**/*.tsx"], + "exclude": [] +} From 812d3dcbdfdbb825eee47eb430c7ca0ec69b92c1 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 13 Sep 2023 16:54:34 +0200 Subject: [PATCH 12/12] Update src/testing/internal/disposables/withCleanup.ts --- src/testing/internal/disposables/withCleanup.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/testing/internal/disposables/withCleanup.ts b/src/testing/internal/disposables/withCleanup.ts index 38a633e28f7..f50ba280c1d 100644 --- a/src/testing/internal/disposables/withCleanup.ts +++ b/src/testing/internal/disposables/withCleanup.ts @@ -7,6 +7,8 @@ export function withCleanup( ...item, [Symbol.dispose]() { cleanup(item); + // if `item` already has a cleanup function, we also need to call the original cleanup function + // (e.g. if something is wrapped in `withCleanup` twice) if (Symbol.dispose in item) { (item as Disposable)[Symbol.dispose](); }