diff --git a/.eslintrc b/.eslintrc index 46a5a102e43..dccf775414b 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,25 @@ "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/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/fixtures/file.ts b/eslint-local-rules/fixtures/file.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/eslint-local-rules/fixtures/react.tsx b/eslint-local-rules/fixtures/react.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/eslint-local-rules/fixtures/tsconfig.json b/eslint-local-rules/fixtures/tsconfig.json new file mode 100644 index 00000000000..34e9fbdb577 --- /dev/null +++ b/eslint-local-rules/fixtures/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext" + }, + "include": ["file.ts", "react.tsx"] +} 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..4e041c609c8 --- /dev/null +++ b/eslint-local-rules/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "test": "node -r ts-node/register/transpile-only --no-warnings --test --watch *.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/eslint-local-rules/require-using-disposable.ts b/eslint-local-rules/require-using-disposable.ts new file mode 100644 index 00000000000..35489c166a3 --- /dev/null +++ b/eslint-local-rules/require-using-disposable.ts @@ -0,0 +1,63 @@ +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) { + 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 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 do that right now + typePart.symbol.escapedName === "Disposable" && + node.kind != "using" + ) { + context.report({ + messageId: "missingUsing", + node: declarator, + }); + } + if ( + // similarly 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: [], +}); + +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/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/eslint-local-rules/tsconfig.json b/eslint-local-rules/tsconfig.json new file mode 100644 index 00000000000..a483dbea739 --- /dev/null +++ b/eslint-local-rules/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "noEmit": true + } +} diff --git a/package-lock.json b/package-lock.json index 0fc0e9ff5c7..1bede4eac35 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": "5.62.0", - "@typescript-eslint/parser": "5.62.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,6 +61,7 @@ "eslint": "8.49.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", @@ -68,7 +73,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", @@ -83,6 +88,7 @@ "size-limit": "8.2.6", "subscriptions-transport-ws": "0.11.0", "terser": "5.19.2", + "ts-api-utils": "1.0.3", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", "ts-node": "10.9.1", @@ -1285,9 +1291,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" @@ -2507,6 +2513,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", @@ -2682,9 +2694,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 +2849,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.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.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.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.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": { @@ -2886,25 +2899,26 @@ } }, "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.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": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.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": { - "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": { @@ -2912,17 +2926,56 @@ } } }, + "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/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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@eslint/eslintrc": ">=2", + "eslint": ">=8" + } + }, + "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==", + "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", - "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", @@ -2930,25 +2983,25 @@ } }, "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.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": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/utils": "6.6.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": { @@ -2957,12 +3010,12 @@ } }, "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==", + "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": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2970,21 +3023,21 @@ } }, "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==", + "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": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.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", - "semver": "^7.3.7", - "tsutils": "^3.21.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", @@ -3012,57 +3065,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" @@ -3075,16 +3105,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", @@ -4856,6 +4886,12 @@ "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", @@ -4872,6 +4908,143 @@ "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", @@ -5980,9 +6153,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 +8463,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 +9177,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 +10612,18 @@ "node": ">=8" } }, + "node_modules/ts-api-utils": { + "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" + }, + "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 075d2e16762..3114d319890 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": "5.62.0", - "@typescript-eslint/parser": "5.62.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,6 +138,7 @@ "eslint": "8.49.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", @@ -145,7 +150,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", @@ -160,6 +165,7 @@ "size-limit": "8.2.6", "subscriptions-transport-ws": "0.11.0", "terser": "5.19.2", + "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 613f5829b38..50c668f934c 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.takeSnapshots("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.takeSnapshots("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..eb13692d37b 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,10 +2879,9 @@ describe("client", () => { .then(resolve, reject); }); - withErrorSpy( - itAsync, - "should warn if server returns wrong data", - (resolve, reject) => { + it("should warn if server returns wrong data", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + await new Promise((resolve, reject) => { const query = gql` query { todos { @@ -2924,8 +2923,8 @@ describe("client", () => { 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__/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/__tests__/local-state/__snapshots__/export.ts.snap b/src/__tests__/local-state/__snapshots__/export.ts.snap index b73dd28148c..71ccfba31d1 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, - }, ], } `; @@ -62,75 +51,14 @@ exports[`@client @export tests should refetch if an @export variable changes, th "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, + "currentAuthorId": 100, }, ], Array [ "Missing field '%s' while writing result %o", "postCount", Object { - "currentAuthorId": 100, + "currentAuthorId": 101, }, ], ], @@ -143,30 +71,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, - }, ], } `; diff --git a/src/__tests__/local-state/export.ts b/src/__tests__/local-state/export.ts index 9b2a27dc962..ea3fb15ae5b 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,9 @@ describe("@client @export tests", () => { } ); - withErrorSpy( - itAsync, - "should allow @client @export variables to be used with remote queries", - (resolve, reject) => { + 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 { @@ -230,8 +230,8 @@ describe("@client @export tests", () => { }); resolve(); }); - } - ); + }); + }); itAsync( "should support @client @export variables that are nested multiple " + @@ -728,137 +728,141 @@ describe("@client @export tests", () => { } ); - withErrorSpy( - 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) => { - 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; + }, + }); }); } ); - withErrorSpy( - 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) => { - 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 4cdb0adb96a..30d31feea33 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,9 @@ describe("Combining client and server state/operations", () => { } ); - withErrorSpy( - itAsync, - "should handle a simple query with both server and client fields", - (resolve, reject) => { + 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 @@ -1050,13 +1050,12 @@ describe("Combining client and server state/operations", () => { resolve(); }, }); - } - ); + }); + }); - withErrorSpy( - itAsync, - "should support nested querying of both server and client fields", - (resolve, reject) => { + 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 { @@ -1116,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 115902f14c5..d7656a9232d 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,9 @@ describe("mutation results", () => { } ); - withErrorSpy( - itAsync, - "should warn when the result fields don't match the query fields", - (resolve, reject) => { + 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; @@ -532,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__/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..01beba28898 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.takeSnapshots("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.takeSnapshots("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.takeSnapshots("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.takeSnapshots("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..6f04fabe5d7 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.takeSnapshots("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.takeSnapshots("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..f1911b0a3c4 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.takeSnapshots("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.takeSnapshots("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.takeSnapshots("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.takeSnapshots("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.takeSnapshots("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.takeSnapshots("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/config/jest/setup.ts b/src/config/jest/setup.ts index ed5e66dea24..f5ad519835f 100644 --- a/src/config/jest/setup.ts +++ b/src/config/jest/setup.ts @@ -16,3 +16,14 @@ function fail(reason = "fail was called in a test.") { // @ts-ignore globalThis.fail = fail; + +if (!Symbol.dispose) { + Object.defineProperty(Symbol, "dispose", { + value: Symbol("dispose"), + }); +} +if (!Symbol.asyncDispose) { + Object.defineProperty(Symbol, "asyncDispose", { + value: Symbol("asyncDispose"), + }); +} 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 2f2f17dd39c..08121af7f27 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"); @@ -3771,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(); @@ -3794,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 () => { @@ -4825,7 +4817,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 +4944,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 +4974,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 +5200,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..3ba88663d85 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 () => { @@ -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 () => { @@ -2498,7 +2495,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 +2576,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 +2649,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 +2733,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..d900a2d53fe 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 () => { @@ -4656,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 @@ -4696,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 () => { @@ -4982,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 @@ -5037,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( () => { @@ -5068,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 } @@ -5109,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( () => { @@ -5125,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 @@ -5181,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( () => { @@ -5650,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) { @@ -5709,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", { @@ -5721,7 +5705,6 @@ describe("useQuery Hook", () => { __typename: "Car", } ); - errorSpy.mockRestore(); }); it("should return partial cache data when `returnPartialData` is true", async () => { @@ -8043,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/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..143de49fd7c --- /dev/null +++ b/src/testing/internal/disposables/spyOnConsole.ts @@ -0,0 +1,36 @@ +import { withCleanup } from "./withCleanup.js"; + +const noOp = () => {}; +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 +): Spies & Disposable { + const spies = {} as Spies; + 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); + } + }); +} + +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 new file mode 100644 index 00000000000..f50ba280c1d --- /dev/null +++ b/src/testing/internal/disposables/withCleanup.ts @@ -0,0 +1,17 @@ +/** @internal */ +export function withCleanup( + item: T, + cleanup: (item: T) => void +): T & Disposable { + return { + ...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](); + } + }, + }; +} 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"; 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": [] +}