From 6e3011a81f9cb2beba0b7248500fbd1bf94369aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= <45039845+notanengineercom@users.noreply.github.com> Date: Thu, 2 Jun 2022 09:26:34 +0200 Subject: [PATCH] [ReleaseBranch] Substitute v2-beta: Rework substitution logic (#139) * add ts-node * create linked list classes * minor refactoring * add root substitute graph * replace context with contextNode * add recorded arguments class * add working returns example * update dependencies * add compile key to ava config * refactor existing interfaces This includes SubstituteBase, SubstituteException, Arguments, Utilities * replace linked list implementations with recorder + node * rework substitute implementation * refactor Arguments.ts * Substitute v2-beta: Tests (#231) * move existing tests to regression folder * add RecordedArguments spec * create Utilities spec * Improve perfomance: implement RecordsSet (#232) * improve proxy creation function * use node contexts to simplify node logic * implement custom records set RecordsSet implements the higher order filter and map methods which get applied when retrieving the iterator. This increases performance as it doesn't create arrays on each .map or .filter -> the iterator yields only the end values with one iteration * Add clear substitute (#233) resolves #46 * implement clearSubstitute * add clearSubstitute spec * 2.0.0-beta.0 * refactor and add recorder related specs (#236) * Update package.json Co-authored-by: Mathias Lykkegaard Lorenzen --- package-lock.json | 591 ++++++++++++++---------- package.json | 13 +- spec/Arguments.spec.ts | 111 ----- spec/ClearSubstitute.spec.ts | 65 +++ spec/RecordedArguments.spec.ts | 138 ++++++ spec/Recorder.spec.ts | 80 ++++ spec/RecordsSet.spec.ts | 66 +++ spec/Utilities.spec.ts | 103 +---- spec/didNotReceive.spec.ts | 52 --- spec/index.test.ts | 152 ------ spec/issues/11.test.ts | 28 -- spec/issues/23.test.ts | 25 - spec/issues/45.test.ts | 36 -- spec/mimicks.spec.ts | 65 --- spec/received.spec.ts | 139 ------ spec/regression/Arguments.spec.ts | 110 +++++ spec/regression/didNotReceive.spec.ts | 52 +++ spec/regression/index.test.ts | 155 +++++++ spec/regression/issues/11.test.ts | 28 ++ spec/regression/issues/23.test.ts | 25 + spec/{ => regression}/issues/36.test.ts | 50 +- spec/regression/issues/45.test.ts | 36 ++ spec/{ => regression}/issues/59.test.ts | 6 +- spec/regression/mimicks.spec.ts | 65 +++ spec/regression/received.spec.ts | 137 ++++++ spec/regression/rejects.spec.ts | 48 ++ spec/regression/resolves.spec.ts | 49 ++ spec/regression/returns.spec.ts | 184 ++++++++ spec/{ => regression}/throws.spec.ts | 2 +- spec/rejects.spec.ts | 52 --- spec/resolves.spec.ts | 49 -- spec/returns.spec.ts | 186 -------- spec/util/compatibility.ts | 6 - src/Arguments.ts | 169 ++++--- src/Context.ts | 128 ----- src/RecordedArguments.ts | 93 ++++ src/Recorder.ts | 54 +++ src/RecordsSet.ts | 65 +++ src/Substitute.ts | 120 +++-- src/SubstituteBase.ts | 86 +--- src/SubstituteException.ts | 52 +++ src/SubstituteNode.ts | 206 +++++++++ src/SubstituteNodeBase.ts | 67 +++ src/SubstituteProxy.ts | 32 ++ src/Transformations.ts | 6 +- src/Utilities.ts | 143 ++---- src/index.ts | 8 +- src/states/ContextState.ts | 10 - src/states/GetPropertyState.ts | 131 ------ src/states/InitialState.ts | 138 ------ src/states/SetPropertyState.ts | 58 --- 51 files changed, 2431 insertions(+), 2039 deletions(-) delete mode 100644 spec/Arguments.spec.ts create mode 100644 spec/ClearSubstitute.spec.ts create mode 100644 spec/RecordedArguments.spec.ts create mode 100644 spec/Recorder.spec.ts create mode 100644 spec/RecordsSet.spec.ts delete mode 100644 spec/didNotReceive.spec.ts delete mode 100644 spec/index.test.ts delete mode 100644 spec/issues/11.test.ts delete mode 100644 spec/issues/23.test.ts delete mode 100644 spec/issues/45.test.ts delete mode 100644 spec/mimicks.spec.ts delete mode 100644 spec/received.spec.ts create mode 100644 spec/regression/Arguments.spec.ts create mode 100644 spec/regression/didNotReceive.spec.ts create mode 100644 spec/regression/index.test.ts create mode 100644 spec/regression/issues/11.test.ts create mode 100644 spec/regression/issues/23.test.ts rename spec/{ => regression}/issues/36.test.ts (57%) create mode 100644 spec/regression/issues/45.test.ts rename spec/{ => regression}/issues/59.test.ts (76%) create mode 100644 spec/regression/mimicks.spec.ts create mode 100644 spec/regression/received.spec.ts create mode 100644 spec/regression/rejects.spec.ts create mode 100644 spec/regression/resolves.spec.ts create mode 100644 spec/regression/returns.spec.ts rename spec/{ => regression}/throws.spec.ts (97%) delete mode 100644 spec/rejects.spec.ts delete mode 100644 spec/resolves.spec.ts delete mode 100644 spec/returns.spec.ts delete mode 100644 spec/util/compatibility.ts delete mode 100644 src/Context.ts create mode 100644 src/RecordedArguments.ts create mode 100644 src/Recorder.ts create mode 100644 src/RecordsSet.ts create mode 100644 src/SubstituteException.ts create mode 100644 src/SubstituteNode.ts create mode 100644 src/SubstituteNodeBase.ts create mode 100644 src/SubstituteProxy.ts delete mode 100644 src/states/ContextState.ts delete mode 100644 src/states/GetPropertyState.ts delete mode 100644 src/states/InitialState.ts delete mode 100644 src/states/SetPropertyState.ts diff --git a/package-lock.json b/package-lock.json index e1e1e0d..885b5f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,40 +1,41 @@ { "name": "@fluffy-spoon/substitute", - "version": "1.0.0", + "version": "2.0.0-beta.0", "lockfileVersion": 1, "requires": true, "dependencies": { "@ava/typescript": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@ava/typescript/-/typescript-1.1.1.tgz", - "integrity": "sha512-KbLUAe2cWXK63WLK6LnOJonjwEDU/8MNXCOA1ooX/YFZgKRmeAD1kZu+2K0ks5fnOCEcckNQAooyBNGdZUmMQA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ava/typescript/-/typescript-2.0.0.tgz", + "integrity": "sha512-sn+upcMk81AMrlnx/hb/9T7gCGuBfw7hi+p79NPSSQMvY2G64mOB7qRaDExiHiZfZ7FN9j7HwQeFhHZLGD/NWQ==", "dev": true, "requires": { - "escape-string-regexp": "^2.0.0" + "escape-string-regexp": "^4.0.0", + "execa": "^5.0.0" } }, "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", "dev": true, "requires": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.14.5" } }, "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", "dev": true }, "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -115,28 +116,28 @@ } }, "@nodelib/fs.scandir": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", - "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "requires": { - "@nodelib/fs.stat": "2.0.4", + "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "@nodelib/fs.stat": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", - "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true }, "@nodelib/fs.walk": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", - "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "requires": { - "@nodelib/fs.scandir": "2.1.4", + "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, @@ -156,27 +157,27 @@ } }, "@types/node": { - "version": "16.4.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.10.tgz", - "integrity": "sha512-TmVHsm43br64js9BqHWqiDZA+xMtbUpI1MBIA0EyiBmoV9pcEYFOSdj5fr6enZNfh4fChh+AGOLIzGwJnkshyQ==", + "version": "12.20.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.24.tgz", + "integrity": "sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ==", "dev": true }, "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, "acorn": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.4.tgz", - "integrity": "sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", "dev": true }, "acorn-walk": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.0.tgz", - "integrity": "sha512-oZRad/3SMOI/pxbbmqyurIx7jHw1wZDcR9G44L8pUVFEomX/0dH89SrM1KaDXuv1NpzAXz6Op/Xu/Qd5XXzdEA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, "aggregate-error": { @@ -245,15 +246,15 @@ "dev": true }, "ansi-styles": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.0.0.tgz", - "integrity": "sha512-6564t0m0fuQMnockqBv7wJxo9T5C2V9JpYXyNScfRDPVLusOQQhkpMGrFC17QbiolraQ1sMXX+Y5nJpjqozL4g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -364,9 +365,9 @@ } }, "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, "base64-js": { @@ -376,15 +377,15 @@ "dev": true }, "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, "bl": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "requires": { "buffer": "^5.5.0", @@ -399,44 +400,25 @@ "dev": true }, "boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.0.1.tgz", + "integrity": "sha512-49VBlw+PrWEF51aCmy7QIteYPIFZxSpvqBdP/2itCPPlJ49kj9zg/XPRFrdkne2W+CfwXUls8exMvu1RysZpKA==", "dev": true, "requires": { "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.0", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true } } @@ -471,9 +453,9 @@ } }, "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, "cacheable-request": { @@ -515,15 +497,15 @@ "dev": true }, "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", "dev": true }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -542,19 +524,19 @@ } }, "chokidar": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", - "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", "dev": true, "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" + "readdirp": "~3.6.0" } }, "chunkd": { @@ -603,9 +585,9 @@ } }, "cli-spinners": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz", - "integrity": "sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz", + "integrity": "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==", "dev": true }, "cli-truncate": { @@ -681,9 +663,9 @@ "dev": true }, "concordance": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.1.tgz", - "integrity": "sha512-TbNtInKVElgEBnJ1v2Xg+MFX2lvFLbmlv3EuSC5wTfCwpB8kC3w3mffF6cKuUhkn475Ym1f1I4qmuXzx2+uXpw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", + "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", "dev": true, "requires": { "date-time": "^3.1.0", @@ -711,9 +693,9 @@ } }, "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "dev": true, "requires": { "safe-buffer": "~5.1.1" @@ -725,6 +707,17 @@ "integrity": "sha1-fj5Iu+bZl7FBfdyihoIEtNPYVxU=", "dev": true }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -750,9 +743,9 @@ } }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { "ms": "2.1.2" @@ -885,9 +878,9 @@ "dev": true }, "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, "esprima": { @@ -902,6 +895,23 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, "fast-diff": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", @@ -909,23 +919,22 @@ "dev": true }, "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" } }, "fastq": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.0.tgz", - "integrity": "sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -974,9 +983,9 @@ "dev": true }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "optional": true }, @@ -993,18 +1002,15 @@ "dev": true }, "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -1025,18 +1031,18 @@ } }, "global-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", - "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", "dev": true, "requires": { - "ini": "1.3.7" + "ini": "2.0.0" } }, "globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", "dev": true, "requires": { "array-union": "^2.1.0", @@ -1064,12 +1070,23 @@ "p-cancelable": "^1.0.0", "to-readable-stream": "^1.0.0", "url-parse-lax": "^3.0.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } } }, "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "dev": true }, "has": { @@ -1105,6 +1122,12 @@ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", "dev": true }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1168,15 +1191,15 @@ "dev": true }, "ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true }, "irregular-plurals": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.2.0.tgz", - "integrity": "sha512-YqTdPLfwP7YFN0SsD3QUVCkm9ZG2VzOXv3DOrw5G5mkMbVwptTwVcFv7/C0vOpBmgTxAeTG19XpUs1E522LW9Q==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.3.0.tgz", + "integrity": "sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==", "dev": true }, "is-arrayish": { @@ -1204,9 +1227,9 @@ } }, "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", "dev": true, "requires": { "has": "^1.0.3" @@ -1240,13 +1263,13 @@ } }, "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "dev": true, "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" } }, "is-interactive": { @@ -1280,9 +1303,9 @@ "dev": true }, "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, "is-plain-object": { @@ -1297,18 +1320,36 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "is-yarn-global": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", "dev": true }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -1402,12 +1443,13 @@ "dev": true }, "log-symbols": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", - "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "requires": { - "chalk": "^4.0.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" } }, "lowercase-keys": { @@ -1458,14 +1500,6 @@ "dev": true, "requires": { "escape-string-regexp": "^4.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - } } }, "md5-hex": { @@ -1478,9 +1512,9 @@ } }, "mem": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-8.0.0.tgz", - "integrity": "sha512-qrcJOe6uD+EW8Wrci1Vdiua/15Xw3n/QnaNXE7varnB6InxSk7nu3/i5jfy3S6kWxr8WYJ6R1o0afMUtvorTsA==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", "dev": true, "requires": { "map-age-cleaner": "^0.1.3", @@ -1495,6 +1529,12 @@ } } }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1502,13 +1542,13 @@ "dev": true }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, "requires": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" } }, "mimic-fn": { @@ -1576,6 +1616,15 @@ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1595,17 +1644,18 @@ } }, "ora": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.2.0.tgz", - "integrity": "sha512-+wG2v8TUU8EgzPHun1k/n45pXquQ9fHnbXVetl9rRgO6kjZszGGbraF3XPTIdgeA+s1lbRjSEftAnyT0w8ZMvQ==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "dev": true, "requires": { - "bl": "^4.0.3", + "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", - "log-symbols": "^4.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } @@ -1727,10 +1777,16 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-type": { @@ -1740,9 +1796,9 @@ "dev": true }, "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, "pify": { @@ -1849,6 +1905,12 @@ "escape-goat": "^2.0.0" } }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -1859,6 +1921,14 @@ "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + } } }, "read-pkg": { @@ -1874,9 +1944,9 @@ }, "dependencies": { "parse-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", - "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -1905,9 +1975,9 @@ } }, "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { "picomatch": "^2.2.1" @@ -1938,12 +2008,12 @@ "dev": true }, "resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "dev": true, "requires": { - "is-core-module": "^2.1.0", + "is-core-module": "^2.2.0", "path-parse": "^1.0.6" } }, @@ -1997,10 +2067,13 @@ } }, "run-parallel": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", - "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } }, "safe-buffer": { "version": "5.1.2", @@ -2009,9 +2082,9 @@ "dev": true }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -2051,6 +2124,21 @@ } } }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -2092,9 +2180,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -2128,9 +2216,9 @@ } }, "spdx-license-ids": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", - "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", + "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==", "dev": true }, "sprintf-js": { @@ -2146,12 +2234,20 @@ "dev": true, "requires": { "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } } }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -2191,6 +2287,12 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -2225,12 +2327,6 @@ "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", "dev": true }, - "term-size": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", - "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", - "dev": true - }, "time-zone": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", @@ -2274,9 +2370,9 @@ } }, "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", "dev": true }, "unique-string": { @@ -2289,23 +2385,23 @@ } }, "update-notifier": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.0.1.tgz", - "integrity": "sha512-BuVpRdlwxeIOvmc32AGYvO1KVdPlsmqSh8KDDBxS6kDE5VR7R8OMP1d8MdhaVBvxl4H3551k9akXr0Y1iIB2Wg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", "dev": true, "requires": { - "boxen": "^4.2.0", + "boxen": "^5.0.0", "chalk": "^4.1.0", "configstore": "^5.0.1", "has-yarn": "^2.1.0", "import-lazy": "^2.1.0", "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.2", + "is-installed-globally": "^0.4.0", "is-npm": "^5.0.0", "is-yarn-global": "^0.3.0", "latest-version": "^5.1.0", "pupa": "^2.1.1", - "semver": "^7.3.2", + "semver": "^7.3.4", "semver-diff": "^3.1.1", "xdg-basedir": "^4.0.0" } @@ -2350,6 +2446,15 @@ "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", "dev": true }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, "widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -2406,9 +2511,9 @@ "dev": true }, "y18n": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", - "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yallist": { @@ -2433,9 +2538,9 @@ } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true } } diff --git a/package.json b/package.json index 4f9dbdc..53ba734 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fluffy-spoon/substitute", - "version": "1.0.0", + "version": "2.0.0-beta.1", "description": "TypeScript port of NSubstitute, which aims to provide a much more fluent mocking opportunity for strong-typed languages", "license": "MIT", "funding": { @@ -45,19 +45,20 @@ }, "dependencies": {}, "devDependencies": { - "@ava/typescript": "^1.1.0", - "@types/node": "^16.4.10", + "@ava/typescript": "^2.0.0", + "@types/node": "^12.20.24", "ava": "^3.15.0", - "typescript": "^4.3.5" + "typescript": "^4.4.3" }, "ava": { "typescript": { "rewritePaths": { "/": "dist/" - } + }, + "compile": false }, "cache": false, "failFast": true, "failWithoutAssertions": true } -} \ No newline at end of file +} diff --git a/spec/Arguments.spec.ts b/spec/Arguments.spec.ts deleted file mode 100644 index 6efa1c6..0000000 --- a/spec/Arguments.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import test from 'ava'; -import { Arg } from '../src'; -import { Argument } from 'src/Arguments'; - -const testObject = { "foo": "bar" }; -const testArray = ["a", 1, true]; - -const parent = {} as any; -parent.child = parent; -const root = {} as any; -root.path = { to: { nested: root } }; -const testFunc = () => { }; - -test('should match any argument(s) using Arg.all', t => { - t.true(Arg.all().matches([])); - t.true(Arg.all().matches([0])); - t.true(Arg.all().matches([1])); - t.true(Arg.all().matches(['string'])); - t.true(Arg.all().matches([true])); - t.true(Arg.all().matches([false])); - t.true(Arg.all().matches(null)); - t.true(Arg.all().matches(undefined)); - t.true(Arg.all().matches([1, 2])); - t.true(Arg.all().matches(['string1', 'string2'])); -}) - -test('should match any argument using Arg.any', t => { - t.true(Arg.any().matches('hi')); - t.true(Arg.any().matches(1)); - t.true(Arg.any().matches(0)); - t.true(Arg.any().matches(false)); - t.true(Arg.any().matches(true)); - t.true(Arg.any().matches(null)); - t.true(Arg.any().matches(undefined)); - t.true(Arg.any().matches(testObject)); - t.true(Arg.any().matches(testArray)); - t.true(Arg.any().matches(testFunc)); - t.true(Arg.any().matches()); - t.true(Arg.any().matches(parent)); - t.true(Arg.any().matches(root)); - t.true(Arg.any().matches(parent)); - t.true(Arg.any().matches(root)); -}); - -test('should not match any argument using Arg.any.not', t => { - t.false(Arg.any.not().matches('hi')); - t.false(Arg.any.not().matches(1)); - t.false(Arg.any.not().matches(0)); - t.false(Arg.any.not().matches(false)); - t.false(Arg.any.not().matches(true)); - t.false(Arg.any.not().matches(null)); - t.false(Arg.any.not().matches(undefined)); - t.false(Arg.any.not().matches(testObject)); - t.false(Arg.any.not().matches(testArray)); - t.false(Arg.any.not().matches(testFunc)); - t.false(Arg.any.not().matches()); - t.false(Arg.any.not().matches(parent)); - t.false(Arg.any.not().matches(root)); - t.false(Arg.any.not().matches(parent)); - t.false(Arg.any.not().matches(root)); -}); - -test('should match the type of the argument using Arg.any', t => { - t.true(Arg.any('string').matches('foo')); - t.true(Arg.any('number').matches(1)); - t.true(Arg.any('boolean').matches(true)); - t.true(Arg.any('symbol').matches(Symbol())); - t.true((>Arg.any('undefined')).matches(undefined)); - t.true(Arg.any('object').matches(testObject)); - t.true(Arg.any('array').matches(testArray)); - t.true(Arg.any('function').matches(testFunc)); - t.true(Arg.any('object').matches(parent)); - t.true(Arg.any('object').matches(root)); - - t.false((>Arg.any('string')).matches(1)); - t.false((>Arg.any('number')).matches('string')); - t.false(Arg.any('boolean').matches(null)); - t.false((>Arg.any('object')).matches('foo')); - t.false((>Arg.any('array')).matches('bar')); - t.false((>Arg.any('function')).matches('foo')); -}); - - -test('should not match the type of the argument using Arg.any.not', t => { - t.false(Arg.any.not('string').matches('123')); - t.false(Arg.any.not('number').matches(123)); - t.false(Arg.any.not('boolean').matches(true)); - t.false(Arg.any.not('symbol').matches(Symbol())); - t.false((>Arg.any.not('undefined')).matches(undefined)); - t.false(Arg.any.not('object').matches(testObject)); - t.false(Arg.any.not('array').matches(testArray)); - t.false(Arg.any.not('function').matches(testFunc)); - t.false(Arg.any.not('object').matches(parent)); - t.false(Arg.any.not('object').matches(root)); -}); - -test('should match the argument with the predicate function using Arg.is', t => { - t.true(Arg.is(x => x === 'foo').matches('foo')); - t.true(Arg.is(x => x % 2 == 0).matches(4)); - - t.false(Arg.is(x => x === 'foo').matches('bar')); - t.false(Arg.is(x => x % 2 == 0).matches(3)); -}); - -test('should not match the argument with the predicate function using Arg.is.not', t => { - t.false(Arg.is.not(x => x === 'foo').matches('foo')); - t.false(Arg.is.not(x => x % 2 == 0).matches(4)); - - t.true(Arg.is.not(x => x === 'foo').matches('bar')); - t.true(Arg.is.not(x => x % 2 == 0).matches(3)); -}); \ No newline at end of file diff --git a/spec/ClearSubstitute.spec.ts b/spec/ClearSubstitute.spec.ts new file mode 100644 index 0000000..b587d4c --- /dev/null +++ b/spec/ClearSubstitute.spec.ts @@ -0,0 +1,65 @@ +import test from 'ava' + +import { Substitute, SubstituteOf } from '../src' +import { SubstituteBase } from '../src/SubstituteBase' +import { SubstituteNode } from '../src/SubstituteNode' + +interface Calculator { + add(a: number, b: number): number + subtract(a: number, b: number): number + divide(a: number, b: number): number + isEnabled: boolean +} + +type InstanceReturningSubstitute = SubstituteOf & { + [SubstituteBase.instance]: Substitute +} + +test('clears everything on a substitute', t => { + const calculator = Substitute.for() as InstanceReturningSubstitute + calculator.add(1, 1) + calculator.received().add(1, 1) + calculator.clearSubstitute() + + t.is(calculator[Substitute.instance].recorder.records.size, 0) + t.is(calculator[Substitute.instance].recorder.indexedRecords.size, 0) + + t.throws(() => calculator.received().add(1, 1)) + + // explicitly using 'all' + calculator.add(1, 1) + calculator.received().add(1, 1) + calculator.clearSubstitute('all') + + t.is(calculator[Substitute.instance].recorder.records.size, 0) + t.is(calculator[Substitute.instance].recorder.indexedRecords.size, 0) + + t.throws(() => calculator.received().add(1, 1)) +}) + +test('clears received calls on a substitute', t => { + const calculator = Substitute.for() as InstanceReturningSubstitute + calculator.add(1, 1) + calculator.add(1, 1).returns(2) + calculator.clearSubstitute('receivedCalls') + + t.is(calculator[Substitute.instance].recorder.records.size, 2) + t.is(calculator[Substitute.instance].recorder.indexedRecords.size, 2) + + t.throws(() => calculator.received().add(1, 1)) + t.is(calculator.add(1, 1), 2) +}) + +test('clears return values on a substitute', t => { + const calculator = Substitute.for() as InstanceReturningSubstitute + calculator.add(1, 1) + calculator.add(1, 1).returns(2) + calculator.clearSubstitute('substituteValues') + + t.is(calculator[Substitute.instance].recorder.records.size, 2) + t.is(calculator[Substitute.instance].recorder.indexedRecords.size, 2) + + t.notThrows(() => calculator.received().add(1, 1)) + // @ts-expect-error + t.true(calculator.add(1, 1)[SubstituteBase.instance] instanceof SubstituteNode) +}) \ No newline at end of file diff --git a/spec/RecordedArguments.spec.ts b/spec/RecordedArguments.spec.ts new file mode 100644 index 0000000..d661585 --- /dev/null +++ b/spec/RecordedArguments.spec.ts @@ -0,0 +1,138 @@ +import test from 'ava' +import { inspect } from 'util' + +import { Arg } from '../src' +import { RecordedArguments } from '../src/RecordedArguments' + +const testObject = { 'foo': 'bar' } +const testArray = ['a', 1, true] + +// #90: Infinite recursion in deepEqual https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/blob/master/spec/issues/90.test.ts +const parent = {} as any +parent.child = parent +const root = {} as any +root.path = { to: { nested: root } } + +const testFunc = () => { } +const testSymbol = Symbol() + +test('records values and classifies them correctly', t => { + const emptyArguments = RecordedArguments.from([]) + t.deepEqual(emptyArguments.value, []) + t.is(emptyArguments.argumentsClass, 'plain') + t.is(emptyArguments.hasNoArguments, false) + + const primitivesOnlyArguments = RecordedArguments.from([1, 'Substitute', false, testSymbol, undefined, null, testFunc, {}]) + t.deepEqual(primitivesOnlyArguments.value, [1, 'Substitute', false, testSymbol, undefined, null, testFunc, {}]) + t.is(primitivesOnlyArguments.argumentsClass, 'plain') + t.is(primitivesOnlyArguments.hasNoArguments, false) + + const anyArg = Arg.any('any') + const withSingleArgumentArguments = RecordedArguments.from([1, 'Substitute', false, testSymbol, undefined, null, testFunc, {}, anyArg]) + t.deepEqual(withSingleArgumentArguments.value, [1, 'Substitute', false, testSymbol, undefined, null, testFunc, {}, anyArg]) + t.is(withSingleArgumentArguments.argumentsClass, 'with-predicate') + t.is(withSingleArgumentArguments.hasNoArguments, false) + + const allArg = Arg.all() + const allArgumentArguments = RecordedArguments.from([allArg]) + t.deepEqual(allArgumentArguments.value, [allArg]) + t.is(allArgumentArguments.argumentsClass, 'wildcard') + t.is(allArgumentArguments.hasNoArguments, false) +}) + +test('creates a valid instance for no arguments', t => { + const args = RecordedArguments.none() + + t.is(args.value, undefined) + t.is(args.argumentsClass, undefined) + t.is(args.hasNoArguments, true) +}) + +test('sorts correctly objects with RecordedArguments', t => { + const plain1 = RecordedArguments.from([]) + const plain2 = RecordedArguments.from([1, 2]) + const withPredicate1 = RecordedArguments.from([1, Arg.any()]) + const withPredicate2 = RecordedArguments.from([Arg.any()]) + const wildcard1 = RecordedArguments.from([Arg.all()]) + const wildcard2 = RecordedArguments.from([Arg.all()]) + + const wrapper = (recordedArguments: RecordedArguments[]) => recordedArguments.map(args => ({ recordedArguments: args })) + const sortedArgs1 = RecordedArguments.sort(wrapper([wildcard1, wildcard2, withPredicate1, withPredicate2, plain1, plain2])) + const sortedArgs2 = RecordedArguments.sort(wrapper([wildcard1, withPredicate1, plain1, withPredicate2, wildcard2, plain2])) + + t.deepEqual(sortedArgs1, wrapper([plain1, plain2, withPredicate1, withPredicate2, wildcard1, wildcard2])) + t.deepEqual(sortedArgs2, wrapper([plain1, plain2, withPredicate1, withPredicate2, wildcard1, wildcard2])) +}) + +test('matches correctly with another RecordedArguments instance when none arguments are recorded', t => { + const args = RecordedArguments.none() + + t.true(args.match(args)) + t.true(args.match(RecordedArguments.none())) + + t.false(args.match(RecordedArguments.from([]))) + t.false(RecordedArguments.from([]).match(args)) + t.false(args.match(RecordedArguments.from([undefined]))) +}) + +test('matches correctly with another RecordedArguments instance when primitive arguments are recorded', t => { + // single + t.true(RecordedArguments.from([]).match(RecordedArguments.from([]))) + t.true(RecordedArguments.from(['Substitute']).match(RecordedArguments.from(['Substitute']))) + t.true(RecordedArguments.from([0]).match(RecordedArguments.from([0]))) + t.true(RecordedArguments.from([true]).match(RecordedArguments.from([true]))) + t.true(RecordedArguments.from([false]).match(RecordedArguments.from([false]))) + t.true(RecordedArguments.from([undefined]).match(RecordedArguments.from([undefined]))) + t.true(RecordedArguments.from([null]).match(RecordedArguments.from([null]))) + t.true(RecordedArguments.from([Symbol.for('test')]).match(RecordedArguments.from([Symbol.for('test')]))) + + t.false(RecordedArguments.from(['a']).match(RecordedArguments.from(['b']))) + t.false(RecordedArguments.from([1]).match(RecordedArguments.from([2]))) + t.false(RecordedArguments.from([true]).match(RecordedArguments.from([false]))) + t.false(RecordedArguments.from([undefined]).match(RecordedArguments.from([null]))) + t.false(RecordedArguments.from(['1']).match(RecordedArguments.from([1]))) + + // multi + t.true(RecordedArguments.from([1, 2, 3]).match(RecordedArguments.from([1, 2, 3]))) + + t.false(RecordedArguments.from([1, 2, 3]).match(RecordedArguments.from([3, 2, 1]))) + t.false(RecordedArguments.from([1, 2, 3]).match(RecordedArguments.from([1, 2, 3, 4]))) + t.false(RecordedArguments.from([1, 2, 3, 4]).match(RecordedArguments.from([1, 2, 3]))) +}) + +test('matches correctly with another RecordedArguments instance when object arguments are recorded', t => { + // same reference + t.true(RecordedArguments.from([testObject]).match(RecordedArguments.from([testObject]))) + t.true(RecordedArguments.from([testArray]).match(RecordedArguments.from([testArray]))) + t.true(RecordedArguments.from([testFunc]).match(RecordedArguments.from([testFunc]))) + t.true(RecordedArguments.from([parent]).match(RecordedArguments.from([parent]))) + t.true(RecordedArguments.from([root]).match(RecordedArguments.from([root]))) + + // deep equal + const objectWithSelfReference = { a: 1, b: 2 } as any + objectWithSelfReference.c = objectWithSelfReference + const anotherObjectWithSelfReference = { a: 1, b: 2 } as any + anotherObjectWithSelfReference.c = anotherObjectWithSelfReference + + t.true(RecordedArguments.from([{ a: 1 }]).match(RecordedArguments.from([{ a: 1 }]))) + t.true(RecordedArguments.from([[]]).match(RecordedArguments.from([[]]))) + t.true(RecordedArguments.from([[1, 'a']]).match(RecordedArguments.from([[1, 'a']]))) + t.true(RecordedArguments.from([objectWithSelfReference]).match(RecordedArguments.from([anotherObjectWithSelfReference]))) +}) + +test('matches correctly with another RecordedArguments instance when using a wildcard argument', t => { + t.true(RecordedArguments.from([Arg.all()]).match(RecordedArguments.from([1, 2, 3]))) + t.true(RecordedArguments.from(['Substitute', 'JS']).match(RecordedArguments.from([Arg.all()]))) +}) + +test('matches correctly with another RecordedArguments instance when using predicate arguments', t => { + t.true(RecordedArguments.from([Arg.any(), Arg.any('number'), Arg.is((x: number) => x === 3), 4]).match(RecordedArguments.from([1, 2, 3, 4]))) + t.true(RecordedArguments.from(['Substitute', 'JS']).match(RecordedArguments.from([Arg.is(x => typeof x === 'string'), Arg.any('string')]))) +}) + +test('generates custom text representation', t => { + t.is(inspect(RecordedArguments.none()), '') + t.is(inspect(RecordedArguments.from([])), '()') + t.is(inspect(RecordedArguments.from([undefined])), 'undefined') + t.is(inspect(RecordedArguments.from([undefined, 1])), '(undefined, 1)') +}) \ No newline at end of file diff --git a/spec/Recorder.spec.ts b/spec/Recorder.spec.ts new file mode 100644 index 0000000..0526185 --- /dev/null +++ b/spec/Recorder.spec.ts @@ -0,0 +1,80 @@ +import test from 'ava' + +import { Recorder } from '../src/Recorder' +import { RecordsSet } from '../src/RecordsSet' +import { Substitute } from '../src/Substitute' +import { SubstituteNodeBase } from '../src/SubstituteNodeBase' + +const nodeFactory = (key: string) => { + const node = Substitute.for() + node.key.returns(key) + return node +} + +const node = nodeFactory('node') +const otherNode = nodeFactory('otherNode') +const otherNodeDifferentInstance = nodeFactory('otherNode') + +test('adds all records once only', t => { + const recorder = new Recorder() + recorder.addRecord(node) + recorder.addRecord(node) + recorder.addRecord(otherNode) + recorder.addRecord(otherNode) + recorder.addRecord(otherNodeDifferentInstance) + + const allRecords = [...recorder.records] + t.deepEqual(allRecords, [node, otherNode, otherNodeDifferentInstance]) +}) + +test('indexes all records correctly', t => { + const recorder = new Recorder() + recorder.addIndexedRecord(node) + recorder.addIndexedRecord(node) + recorder.addIndexedRecord(otherNode) + recorder.addIndexedRecord(otherNode) + recorder.addIndexedRecord(otherNodeDifferentInstance) + + const allRecords = [...recorder.records] + t.deepEqual(allRecords, [node, otherNode, otherNodeDifferentInstance]) + + const nodeSet = recorder.indexedRecords.get(node.key) + t.true(nodeSet instanceof RecordsSet) + t.deepEqual([...nodeSet], [node]) + + const otherNodeSet = recorder.indexedRecords.get(otherNode.key) + t.true(otherNodeSet instanceof RecordsSet) + t.deepEqual([...otherNodeSet], [otherNode, otherNodeDifferentInstance]) +}) + +test('returns all sibling nodes', t => { + const recorder = new Recorder() + recorder.addIndexedRecord(node) + recorder.addIndexedRecord(otherNode) + recorder.addIndexedRecord(otherNodeDifferentInstance) + + const nodeSiblings = recorder.getSiblingsOf(node) + t.deepEqual([...nodeSiblings], []) + + const otherNodeSiblings = recorder.getSiblingsOf(otherNode) + t.deepEqual([...otherNodeSiblings], [otherNodeDifferentInstance]) + + const otherNodeDifferentInstanceSiblings = recorder.getSiblingsOf(otherNodeDifferentInstance) + t.deepEqual([...otherNodeDifferentInstanceSiblings], [otherNode]) +}) + +test('clears recorded nodes by a given filter function', t => { + const recorder = new Recorder() + recorder.addIndexedRecord(node) + recorder.addIndexedRecord(otherNode) + recorder.addIndexedRecord(otherNodeDifferentInstance) + + recorder.clearRecords(n => n.key === otherNode.key) + t.deepEqual([...recorder.records], [node]) + t.deepEqual([...recorder.indexedRecords.get(node.key)], [node]) + t.is(recorder.indexedRecords.get(otherNode.key), undefined) + + recorder.clearRecords(_ => true) + t.deepEqual([...recorder.records], []) + t.deepEqual(recorder.indexedRecords.get(node.key), undefined) +}) diff --git a/spec/RecordsSet.spec.ts b/spec/RecordsSet.spec.ts new file mode 100644 index 0000000..2f90868 --- /dev/null +++ b/spec/RecordsSet.spec.ts @@ -0,0 +1,66 @@ +import test, { ExecutionContext, Macro } from 'ava' + +import { RecordsSet } from '../src/RecordsSet' + +const dataArray = [1, 2, 3] +function* dataArrayGenerator() { + yield* dataArray +} +const inputData = [dataArray, dataArray[Symbol.iterator](), new Set(dataArray), dataArrayGenerator(), new RecordsSet(dataArray)] +const macro: Macro = (t: ExecutionContext, ...inputData: Iterable[]): void => { + inputData.forEach(input => { + const set = new RecordsSet(input) + + t.true(set.has(1)) + t.true(set.has(2)) + t.true(set.has(3)) + t.is(set.size, 3) + t.deepEqual([...set], dataArray) + t.deepEqual([...set[Symbol.iterator]()], dataArray) + t.deepEqual([...set.values()], dataArray) + + t.false(set.delete(4)) + t.true(set.delete(3)) + t.false(set.delete(3)) + t.is(set.size, 2) + + set.clear() + t.is(set.size, 0) + }) +} + +test('behaves like a native Set object', macro, ...inputData) + +test('applies a filter function everytime the iterator is consumed', t => { + const set = new RecordsSet([1, 2, 3]) + const setWithFilter = set.filter(number => number !== 2) + + t.deepEqual([...set], [1, 2, 3]) + t.deepEqual([...setWithFilter], [1, 3]) + t.deepEqual([...setWithFilter], [1, 3]) +}) + +test('applies a map function everytime the iterator is consumed', t => { + const set = new RecordsSet([1, 2, 3]) + const setWithMap = set.map(number => number.toString()) + + t.deepEqual([...set], [1, 2, 3]) + t.deepEqual([...setWithMap], ['1', '2', '3']) + t.deepEqual([...setWithMap], ['1', '2', '3']) +}) + +test('applies and preserves the order of filter and map functions everytime the iterator is consumed', t => { + const set = new RecordsSet([1, 2, 3]) + const setWithFilter = set.filter(number => number !== 2) + const setWithFilterAndMap = setWithFilter.map(number => number.toString()) + const setWithFilterMapAndAnotherFilter = setWithFilterAndMap.filter(string => string === '3') + + t.deepEqual([...set], [1, 2, 3]) + t.deepEqual([...setWithFilter], [1, 3]) + t.deepEqual([...setWithFilterAndMap], ['1', '3']) + t.deepEqual([...setWithFilterMapAndAnotherFilter], ['3']) + + t.deepEqual([...setWithFilter], [1, 3]) + t.deepEqual([...setWithFilterAndMap], ['1', '3']) + t.deepEqual([...setWithFilterMapAndAnotherFilter], ['3']) +}) \ No newline at end of file diff --git a/spec/Utilities.spec.ts b/spec/Utilities.spec.ts index be6ea2b..462b7fd 100644 --- a/spec/Utilities.spec.ts +++ b/spec/Utilities.spec.ts @@ -1,102 +1,5 @@ -import test from 'ava'; +import test from 'ava' -import { Arg } from '../src'; -import { areArgumentArraysEqual } from '../src/Utilities'; - -const testObject = { "foo": "bar" }; -const testArray = ["a", 1, true]; - -// #90: Infinite recursion in deepEqual https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/blob/master/spec/issues/90.test.ts -const parent = {} as any; -parent.child = parent; -const root = {} as any; -root.path = { to: { nested: root } }; - -const testFunc = () => { }; - -//#region areArgumentArraysEqual test('areArgumentArraysEqual should return valid result for primitive arguments', t => { - // single - t.true(areArgumentArraysEqual([''], [''])); - t.true(areArgumentArraysEqual(['a'], ['a'])); - t.true(areArgumentArraysEqual([0], [0])); - t.true(areArgumentArraysEqual([1], [1])); - t.true(areArgumentArraysEqual([true], [true])); - t.true(areArgumentArraysEqual([false], [false])); - t.true(areArgumentArraysEqual([undefined], [undefined])); - t.true(areArgumentArraysEqual([null], [null])); - t.true(areArgumentArraysEqual([testObject], [testObject])); - t.true(areArgumentArraysEqual([testArray], [testArray])); - t.true(areArgumentArraysEqual([testFunc], [testFunc])); - t.true(areArgumentArraysEqual([parent], [parent])); - t.true(areArgumentArraysEqual([root], [root])); - - t.false(areArgumentArraysEqual(['a'], ['b'])); - t.false(areArgumentArraysEqual([1], [2])); - t.false(areArgumentArraysEqual([true], [false])); - t.false(areArgumentArraysEqual([undefined], [null])); - t.false(areArgumentArraysEqual([testObject], [testArray])); - - // multi - t.true(areArgumentArraysEqual([1, 2, 3], [1, 2, 3])); - - t.false(areArgumentArraysEqual([1, 2, 3], [3, 2, 1])); - t.false(areArgumentArraysEqual([1, 2, 3, 4], [1, 2, 3])); - t.false(areArgumentArraysEqual([1, 2, 3], [1, 2, 3, 4])); -}); - -test('areArgumentArraysEqual should return valid result using Arg.all()', t => { - t.true(areArgumentArraysEqual([Arg.all()], [])); - t.true(areArgumentArraysEqual([Arg.all()], [0])); - t.true(areArgumentArraysEqual([Arg.all()], [1])); - t.true(areArgumentArraysEqual([Arg.all()], ['string'])); - t.true(areArgumentArraysEqual([Arg.all()], [true])); - t.true(areArgumentArraysEqual([Arg.all()], [false])); - t.true(areArgumentArraysEqual([Arg.all()], [null])); - t.true(areArgumentArraysEqual([Arg.all()], [undefined])); - t.true(areArgumentArraysEqual([Arg.all()], [1, 2])); - t.true(areArgumentArraysEqual([Arg.all()], ['string1', 'string2'])); - t.true(areArgumentArraysEqual([Arg.all()], [parent, root])); -}) - -test('areArgumentArraysEqual should return valid result using Arg', t => { - t.true(areArgumentArraysEqual([Arg.any()], ['hi'])); - t.true(areArgumentArraysEqual([Arg.any()], [1])); - t.true(areArgumentArraysEqual([Arg.any()], [0])); - t.true(areArgumentArraysEqual([Arg.any()], [false])); - t.true(areArgumentArraysEqual([Arg.any()], [true])); - t.true(areArgumentArraysEqual([Arg.any()], [null])); - t.true(areArgumentArraysEqual([Arg.any()], [undefined])); - t.true(areArgumentArraysEqual([Arg.any()], [testObject])); - t.true(areArgumentArraysEqual([Arg.any()], [testArray])); - t.true(areArgumentArraysEqual([Arg.any()], [testFunc])); - t.true(areArgumentArraysEqual([Arg.any()], [])); - t.true(areArgumentArraysEqual([Arg.any()], [parent])); - t.true(areArgumentArraysEqual([Arg.any()], [root])); - - t.true(areArgumentArraysEqual([Arg.any('string')], ['foo'])); - t.true(areArgumentArraysEqual([Arg.any('number')], [1])); - t.true(areArgumentArraysEqual([Arg.any('boolean')], [true])); - t.true(areArgumentArraysEqual([Arg.any('symbol')], [Symbol()])); - t.true(areArgumentArraysEqual([Arg.any('undefined')], [undefined])); - t.true(areArgumentArraysEqual([Arg.any('object')], [testObject])); - t.true(areArgumentArraysEqual([Arg.any('array')], [testArray])); - t.true(areArgumentArraysEqual([Arg.any('function')], [testFunc])); - t.true(areArgumentArraysEqual([Arg.any('object')], [parent])); - t.true(areArgumentArraysEqual([Arg.any('object')], [root])); - - t.false(areArgumentArraysEqual([Arg.any('string')], [1])); - t.false(areArgumentArraysEqual([Arg.any('number')], ['string'])); - t.false(areArgumentArraysEqual([Arg.any('boolean')], [null])); - t.false(areArgumentArraysEqual([Arg.any('object')], ['foo'])); - t.false(areArgumentArraysEqual([Arg.any('array')], ['bar'])); - t.false(areArgumentArraysEqual([Arg.any('function')], ['foo'])); -}) - -test('areArgumentArraysEqual should return valid result using Arg.is()', t => { - t.true(areArgumentArraysEqual([Arg.is(x => x === 'foo')], ['foo'])); - t.true(areArgumentArraysEqual([Arg.is(x => x % 2 == 0)], [4])); - - t.false(areArgumentArraysEqual([Arg.is(x => x === 'foo')], ['bar'])); - t.false(areArgumentArraysEqual([Arg.is(x => x % 2 == 0)], [3])); -}); \ No newline at end of file + t.pass() +}) \ No newline at end of file diff --git a/spec/didNotReceive.spec.ts b/spec/didNotReceive.spec.ts deleted file mode 100644 index 6333a1f..0000000 --- a/spec/didNotReceive.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import test from 'ava'; -import { Substitute, Arg } from '../src/index'; -import { SubstituteException } from '../src/SubstituteBase'; - -interface Calculator { - add(a: number, b: number): number; - subtract(a: number, b: number): number; - divide(a: number, b: number): number; - isEnabled: boolean; -} - -test('not calling a method correctly asserts the call count', t => { - const calculator = Substitute.for(); - - calculator.didNotReceive().add(1, 1); - t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received().add(Arg.all()), { instanceOf: SubstituteException }); -}); - -test('not getting a property correctly asserts the call count', t => { - const calculator = Substitute.for(); - - calculator.didNotReceive().isEnabled; - t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException }); - t.throws(() => calculator.received().isEnabled, { instanceOf: SubstituteException }); -}); - -test('not setting a property correctly asserts the call count', t => { - const calculator = Substitute.for(); - - calculator.didNotReceive().isEnabled = true; - t.throws(() => calculator.received(1).isEnabled = true, { instanceOf: SubstituteException }); - t.throws(() => calculator.received().isEnabled = true, { instanceOf: SubstituteException }); -}); - -test('not calling a method with mock correctly asserts the call count', t => { - const calculator = Substitute.for(); - calculator.add(1, 1).returns(2); - - calculator.didNotReceive().add(1, 1); - t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received().add(Arg.all()), { instanceOf: SubstituteException }); -}); - -test('not getting a property with mock correctly asserts the call count', t => { - const calculator = Substitute.for(); - calculator.isEnabled.returns(true); - - calculator.didNotReceive().isEnabled; - t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException }); - t.throws(() => calculator.received().isEnabled, { instanceOf: SubstituteException }); -}); \ No newline at end of file diff --git a/spec/index.test.ts b/spec/index.test.ts deleted file mode 100644 index dac9eaa..0000000 --- a/spec/index.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import test from 'ava'; - -import { Substitute, Arg } from '../src'; -import { OmitProxyMethods, ObjectSubstitute } from '../src/Transformations'; - -class Dummy { - -} - -export class Example { - a = "1337"; - - c(arg1: string, arg2: string) { - return "hello " + arg1 + " world (" + arg2 + ")"; - } - - get d() { - return 1337; - } - - set v(x: string | null | undefined) { - } - - received(stuff: number | string) { - - } - - returnPromise() { - return Promise.resolve(new Dummy()); - } - - foo(): string | undefined | null { - return 'stuff'; - } - - bar(a: number, b?: number): number { - return a + b || 0 - } -} - -let instance: Example; -let substitute: ObjectSubstitute, Example>; - -function initialize() { - instance = new Example(); - substitute = Substitute.for(); -}; - -test('class with method called "received" can be used for call count verification when proxies are suspended', t => { - initialize(); - - Substitute.disableFor(substitute).received(2); - - t.throws(() => substitute.received(2).received(2)); - t.notThrows(() => substitute.received(1).received(2)); -}); - -test('class with method called "received" can be used for call count verification', t => { - initialize(); - - Substitute.disableFor(substitute).received('foo'); - - t.notThrows(() => substitute.received(1).received('foo')); - t.throws(() => substitute.received(2).received('foo')); -}); - -test('class string field set received', t => { - initialize(); - - substitute.v = undefined; - substitute.v = null; - substitute.v = 'hello'; - substitute.v = 'hello'; - substitute.v = 'world'; - - t.notThrows(() => substitute.received().v = 'hello'); - t.notThrows(() => substitute.received(5).v = Arg.any()); - t.notThrows(() => substitute.received().v = Arg.any()); - t.notThrows(() => substitute.received(2).v = 'hello'); - t.notThrows(() => substitute.received(2).v = Arg.is(x => x && x.indexOf('ll') > -1)); - - t.throws(() => substitute.received(2).v = Arg.any()); - t.throws(() => substitute.received(1).v = Arg.any()); - t.throws(() => substitute.received(1).v = Arg.is(x => x && x.indexOf('ll') > -1)); - t.throws(() => substitute.received(3).v = 'hello'); -}); - -test('resolving promises works', async t => { - initialize(); - - substitute.returnPromise().resolves(1338); - - t.is(1338, await substitute.returnPromise()); -}); - -test('class void returns', t => { - initialize(); - - substitute.foo().returns(void 0, null); - - t.is(substitute.foo(), void 0); - t.is(substitute.foo(), null); -}); - -test('class method received', t => { - initialize(); - - void substitute.c("hi", "there"); - void substitute.c("hi", "the1re"); - void substitute.c("hi", "there"); - void substitute.c("hi", "there"); - void substitute.c("hi", "there"); - - t.notThrows(() => substitute.received(4).c('hi', 'there')); - t.notThrows(() => substitute.received(1).c('hi', 'the1re')); - t.notThrows(() => substitute.received().c('hi', 'there')); - - const expectedMessage = 'Expected 7 calls to the method c with arguments [\'hi\', \'there\'], but received 4 of such calls.\n' + - 'All calls received to method c:\n' + - '-> call with arguments [\'hi\', \'there\']\n' + - '-> call with arguments [\'hi\', \'the1re\']\n' + - '-> call with arguments [\'hi\', \'there\']\n' + - '-> call with arguments [\'hi\', \'there\']\n' + - '-> call with arguments [\'hi\', \'there\']' - t.throws(() => { substitute.received(7).c('hi', 'there') }, { message: expectedMessage }); -}); - -test('received call matches after partial mocks using property instance mimicks', t => { - initialize(); - - substitute.d.mimicks(() => instance.d); - substitute.c('lala', 'bar'); - - substitute.received(1).c('lala', 'bar'); - substitute.received(1).c('lala', 'bar'); - - t.notThrows(() => substitute.received(1).c('lala', 'bar')); - const expectedMessage = 'Expected 2 calls to the method c with arguments [\'lala\', \'bar\'], but received 1 of such call.\n' + - 'All calls received to method c:\n' + - '-> call with arguments [\'lala\', \'bar\']' - t.throws(() => substitute.received(2).c('lala', 'bar'), { message: expectedMessage }); - - t.deepEqual(substitute.d, 1337); -}); - -test('partial mocks using property instance mimicks', t => { - initialize(); - - substitute.d.mimicks(() => instance.d); - - t.deepEqual(substitute.d, 1337); -}); diff --git a/spec/issues/11.test.ts b/spec/issues/11.test.ts deleted file mode 100644 index eb4d4cd..0000000 --- a/spec/issues/11.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import test from 'ava'; -import { Substitute, Arg } from '../../src/index'; - -type Addands = { - op1: number; - op2: number; -} - -class RealCalculator { - add(addands: Addands): number { - return addands.op1 + addands.op2; - } -} - -test('issue 11: arg.is is only called once', async t => { - let mockedCalculator = Substitute.for(); - mockedCalculator.add(Arg.any()).returns(4); - - let count = 0; - mockedCalculator.add({ op1: 1, op2: 2 }); - - mockedCalculator.received(1).add(Arg.is(a => { - count++; - return a.op1 === 1 && a.op2 === 2; - })); - - t.is(count, 1); -}); \ No newline at end of file diff --git a/spec/issues/23.test.ts b/spec/issues/23.test.ts deleted file mode 100644 index 838bd62..0000000 --- a/spec/issues/23.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import test from "ava"; - -import { Substitute, Arg } from "../../src/index"; - -interface CalculatorInterface { - add(a: number, b: number): number; - subtract(a: number, b: number): number; - divide(a: number, b: number): number; - isEnabled: boolean; -} - -test("issue 23: mimick received should not call method", t => { - const mockedCalculator = Substitute.for(); - - let calls = 0 - - mockedCalculator.add(Arg.all()).mimicks((a, b) => { - t.deepEqual(++calls, 1, 'mimick called twice') - return a + b; - }); - - mockedCalculator.add(1, 1); // ok - - mockedCalculator.received(1).add(1, 1) // not ok, calls mimick func -}); diff --git a/spec/issues/45.test.ts b/spec/issues/45.test.ts deleted file mode 100644 index 34a24fa..0000000 --- a/spec/issues/45.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import test from 'ava'; - -import { Substitute, Arg } from '../../src/index'; - -class DependencyClass { - public methodOne() {} - public methodTwo(someArg: string) {} -} - -class SubjectClass { - private readonly dependency: DependencyClass; - - public constructor(dependency: DependencyClass) { - this.dependency = dependency; - } - - public callToMethodOne() { - this.dependency.methodOne(); - } - public callToMethodTwo() { - this.dependency.methodTwo('string'); - } -} - -test('issue 45 Checking received calls off at times', async t => { - const mock = Substitute.for(); - const subject = new SubjectClass(mock); - - subject.callToMethodOne(); - subject.callToMethodTwo(); - - t.notThrows(() => { - mock.received(1).methodOne(); - mock.received(1).methodTwo(Arg.is(x => x === 'string')); - }); -}); diff --git a/spec/mimicks.spec.ts b/spec/mimicks.spec.ts deleted file mode 100644 index d37bbc5..0000000 --- a/spec/mimicks.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import test from 'ava'; -import { Substitute, Arg } from '../src'; - -interface Calculator { - add(a: number, b: number): number; - multiply(a: number, b?: number): number; - clear(): void; - getMemory(): Promise; - viewResult(back?: number): number; - heavyOperation(...input: number[]): Promise; - isEnabled: boolean; - model: Promise; -}; - -test('mimicks a method with specific arguments', t => { - const calculator = Substitute.for(); - const addMimick = (a: number, b: number) => a + b; - - calculator.add(1, 1).mimicks(addMimick); - t.is(calculator.add(1, 1), 2); -}); - -test('mimicks a method with specific and conditional arguments', t => { - const calculator = Substitute.for(); - const addMimick = (a: number, b: number) => a + b; - - calculator.add(Arg.any('number'), Arg.is((input: number) => input >= 0 && input <= 10)).mimicks(addMimick); - calculator.add(42, -42).mimicks((a: number, b: number) => 0); - - t.is(calculator.add(1234, 6), 1240); - t.is(calculator.add(42, -42), 0); -}); - -test('mimicks a method with Arg.all', t => { - const calculator = Substitute.for(); - const addMimick = (a: number, b: number) => a + b; - - - calculator.add(Arg.all()).mimicks(addMimick); - t.is(calculator.add(42, 58), 100); -}); - -test('mimicks a method with optional arguments', t => { - const calculator = Substitute.for(); - const multiplyOneArgMimicks = (a: number) => a * a; - const multiplyMimicks = (a: number, b?: number) => a * b; - - calculator.multiply(0, Arg.is((b: number) => b > 10 && b < 20)).mimicks(multiplyMimicks); - calculator.multiply(Arg.any('number'), Arg.is((b: number) => b === 2)).mimicks(multiplyMimicks); - calculator.multiply(2).mimicks(multiplyOneArgMimicks); - - t.is(calculator.multiply(0, 13), 0); - t.is(calculator.multiply(42, 2), 84); - t.is(calculator.multiply(2), 4); -}); - -test('mimicks a method where it\'s only argument is optional', t => { - const calculator = Substitute.for(); - - calculator.viewResult().mimicks(() => 0); - calculator.viewResult(3).mimicks(() => 42); - - t.is(calculator.viewResult(), 0); - t.is(calculator.viewResult(3), 42); -}); \ No newline at end of file diff --git a/spec/received.spec.ts b/spec/received.spec.ts deleted file mode 100644 index 65f00db..0000000 --- a/spec/received.spec.ts +++ /dev/null @@ -1,139 +0,0 @@ -import test from 'ava'; -import { Substitute, Arg } from '../src'; -import { SubstituteException } from '../src/SubstituteBase'; - -interface Calculator { - add(a: number, b: number): number; - multiply(a: number, b?: number): number; - isEnabled: boolean; -} - -test('calling a method twice correctly asserts the call count', t => { - const calculator = Substitute.for(); - calculator.add(1, 1); - calculator.add(1, 1); - - calculator.received(2).add(1, 1); - calculator.received().add(1, 1); - t.throws(() => calculator.received(0).add(1, 1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received(2).add(1, 0), { instanceOf: SubstituteException }); - t.throws(() => calculator.received().add(1, 0), { instanceOf: SubstituteException }); -}); - -test('calling a method twice correctly asserts the call count each time', t => { - const calculator = Substitute.for(); - - calculator.add(1, 1); - calculator.received(1).add(1, 1); - - calculator.add(1, 1); - calculator.received(2).add(1, 1); - - t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }); -}); - -test('calling a method with optional arguments correctly asserts the call count', t => { - const calculator = Substitute.for(); - calculator.multiply(1); - calculator.multiply(1, 1); - calculator.multiply(2, 1); - - calculator.received(1).multiply(1); - calculator.received(1).multiply(1, 1); - calculator.received(0).multiply(2); - calculator.received(1).multiply(2, 1); - - t.throws(() => calculator.received(0).multiply(1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received(0).multiply(1, 1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received(2).multiply(1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received(2).multiply(1, 1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received(1).multiply(2), { instanceOf: SubstituteException }); -}); - -test('getting a property twice correctly asserts the call count', t => { - const calculator = Substitute.for(); - calculator.isEnabled; - calculator.isEnabled; - - calculator.received(2).isEnabled; - calculator.received().isEnabled; - t.throws(() => calculator.received(0).isEnabled, { instanceOf: SubstituteException }); - t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException }); -}); - -test('setting a property twice correctly asserts the call count', t => { - const calculator = Substitute.for(); - calculator.isEnabled = true; - calculator.isEnabled = true; - - calculator.received(2).isEnabled = true; - calculator.received().isEnabled = true; - t.throws(() => calculator.received(0).isEnabled = true, { instanceOf: SubstituteException }); - t.throws(() => calculator.received(1).isEnabled = true, { instanceOf: SubstituteException }); - t.throws(() => calculator.received(2).isEnabled = false, { instanceOf: SubstituteException }); - t.throws(() => calculator.received().isEnabled = false, { instanceOf: SubstituteException }); -}); - -test('calling a method twice with mock correctly asserts the call count', t => { - const calculator = Substitute.for(); - calculator.add(1, 1).returns(2); - calculator.add(1, 1); - calculator.add(1, 1); - - calculator.received(2).add(1, 1); - calculator.received().add(1, 1); - t.throws(() => calculator.received(0).add(1, 1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received(2).add(1, 0), { instanceOf: SubstituteException }); - t.throws(() => calculator.received().add(1, 0), { instanceOf: SubstituteException }); -}); - -test('calling a method with mock based on Arg correctly asserts the call count', t => { - const calculator = Substitute.for(); - calculator.add(Arg.all()).returns(0); - calculator.add(1, 1); - - calculator.received().add(Arg.all()); - calculator.received().add(Arg.any(), Arg.any()); - calculator.received().add(Arg.any('number'), Arg.any('number')); - calculator.received().add(1, 1); - ; - t.throws(() => calculator.received().add(1, 0), { instanceOf: SubstituteException }); -}); - -test('getting a property twice with mock correctly asserts the call count', t => { - const calculator = Substitute.for(); - calculator.isEnabled.returns(true); - calculator.isEnabled; - calculator.isEnabled; - - calculator.received(2).isEnabled; - calculator.received().isEnabled; - t.throws(() => calculator.received(0).isEnabled, { instanceOf: SubstituteException }); - t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException }); - t.throws(() => calculator.received().isEnabled, { instanceOf: SubstituteException }); -}); - -test('calling a method with Arg correctly asserts the call count with Arg', t => { - // #18: receive with arg https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/18 - const calculator = Substitute.for(); - calculator.add(Arg.all()); - - calculator.received(1).add(Arg.all()); - calculator.received().add(Arg.all()); - t.throws(() => calculator.received(0).add(1, 1), { instanceOf: SubstituteException }); -}); - -test('calling a method does not interfere with other properties or methods call counts', t => { - // #51: All functions share the same state https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/51 - const calculator = Substitute.for(); - calculator.add(1, 1); - - calculator.received(0).multiply(1, 1); - calculator.received(0).multiply(Arg.all()); - calculator.received(0).isEnabled; - - t.throws(() => calculator.received().multiply(1, 1), { instanceOf: SubstituteException }); - t.throws(() => calculator.received().multiply(Arg.all()), { instanceOf: SubstituteException }); -}); \ No newline at end of file diff --git a/spec/regression/Arguments.spec.ts b/spec/regression/Arguments.spec.ts new file mode 100644 index 0000000..d471db0 --- /dev/null +++ b/spec/regression/Arguments.spec.ts @@ -0,0 +1,110 @@ +import test from 'ava' +import { Arg } from '../../src' +import { Argument } from '../../src/Arguments' + +const testObject = { "foo": "bar" } +const testArray = ["a", 1, true] + +const parent = {} as any +parent.child = parent +const root = {} as any +root.path = { to: { nested: root } } +const testFunc = () => { } + +test('should match any argument(s) using Arg.all', t => { + t.true(Arg.all().matches([])) + t.true(Arg.all().matches([0])) + t.true(Arg.all().matches([1])) + t.true(Arg.all().matches(['string'])) + t.true(Arg.all().matches([true])) + t.true(Arg.all().matches([false])) + t.true(Arg.all().matches(null)) + t.true(Arg.all().matches(undefined)) + t.true(Arg.all().matches([1, 2])) + t.true(Arg.all().matches(['string1', 'string2'])) +}) + +test('should match any argument using Arg.any', t => { + t.true(Arg.any().matches('hi')) + t.true(Arg.any().matches(1)) + t.true(Arg.any().matches(0)) + t.true(Arg.any().matches(false)) + t.true(Arg.any().matches(true)) + t.true(Arg.any().matches(null)) + t.true(Arg.any().matches(undefined)) + t.true(Arg.any().matches(testObject)) + t.true(Arg.any().matches(testArray)) + t.true(Arg.any().matches(testFunc)) + t.true(Arg.any().matches()) + t.true(Arg.any().matches(parent)) + t.true(Arg.any().matches(root)) + t.true(Arg.any().matches(parent)) + t.true(Arg.any().matches(root)) +}) + +test('should not match any argument using Arg.any.not', t => { + t.false(Arg.any.not().matches('hi')) + t.false(Arg.any.not().matches(1)) + t.false(Arg.any.not().matches(0)) + t.false(Arg.any.not().matches(false)) + t.false(Arg.any.not().matches(true)) + t.false(Arg.any.not().matches(null)) + t.false(Arg.any.not().matches(undefined)) + t.false(Arg.any.not().matches(testObject)) + t.false(Arg.any.not().matches(testArray)) + t.false(Arg.any.not().matches(testFunc)) + t.false(Arg.any.not().matches()) + t.false(Arg.any.not().matches(parent)) + t.false(Arg.any.not().matches(root)) + t.false(Arg.any.not().matches(parent)) + t.false(Arg.any.not().matches(root)) +}) + +test('should match the type of the argument using Arg.any', t => { + t.true(Arg.any('string').matches('foo')) + t.true(Arg.any('number').matches(1)) + t.true(Arg.any('boolean').matches(true)) + t.true(Arg.any('symbol').matches(Symbol())) + t.true((>Arg.any('undefined')).matches(undefined)) + t.true(Arg.any('object').matches(testObject)) + t.true(Arg.any('array').matches(testArray)) + t.true(Arg.any('function').matches(testFunc)) + t.true(Arg.any('object').matches(parent)) + t.true(Arg.any('object').matches(root)) + + t.false((>Arg.any('string')).matches(1)) + t.false((>Arg.any('number')).matches('string')) + t.false(Arg.any('boolean').matches(null)) + t.false((>Arg.any('object')).matches('foo')) + t.false((>Arg.any('array')).matches('bar')) + t.false((>Arg.any('function')).matches('foo')) +}) + +test('should not match the type of the argument using Arg.any.not', t => { + t.false(Arg.any.not('string').matches('123')) + t.false(Arg.any.not('number').matches(123)) + t.false(Arg.any.not('boolean').matches(true)) + t.false(Arg.any.not('symbol').matches(Symbol())) + t.false((>Arg.any.not('undefined')).matches(undefined)) + t.false(Arg.any.not('object').matches(testObject)) + t.false(Arg.any.not('array').matches(testArray)) + t.false(Arg.any.not('function').matches(testFunc)) + t.false(Arg.any.not('object').matches(parent)) + t.false(Arg.any.not('object').matches(root)) +}) + +test('should match the argument with the predicate function using Arg.is', t => { + t.true(Arg.is(x => x === 'foo').matches('foo')) + t.true(Arg.is(x => x % 2 == 0).matches(4)) + + t.false(Arg.is(x => x === 'foo').matches('bar')) + t.false(Arg.is(x => x % 2 == 0).matches(3)) +}) + +test('should not match the argument with the predicate function using Arg.is.not', t => { + t.false(Arg.is.not(x => x === 'foo').matches('foo')) + t.false(Arg.is.not(x => x % 2 == 0).matches(4)) + + t.true(Arg.is.not(x => x === 'foo').matches('bar')) + t.true(Arg.is.not(x => x % 2 == 0).matches(3)) +}) \ No newline at end of file diff --git a/spec/regression/didNotReceive.spec.ts b/spec/regression/didNotReceive.spec.ts new file mode 100644 index 0000000..2b01ecc --- /dev/null +++ b/spec/regression/didNotReceive.spec.ts @@ -0,0 +1,52 @@ +import test from 'ava' +import { Substitute, Arg } from '../../src' +import { SubstituteException } from '../../src/SubstituteException' + +interface Calculator { + add(a: number, b: number): number + subtract(a: number, b: number): number + divide(a: number, b: number): number + isEnabled: boolean +} + +test('not calling a method correctly asserts the call count', t => { + const calculator = Substitute.for() + + calculator.didNotReceive().add(1, 1) + t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received().add(Arg.all()), { instanceOf: SubstituteException }) +}) + +test('not getting a property correctly asserts the call count', t => { + const calculator = Substitute.for() + + calculator.didNotReceive().isEnabled + t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException }) + t.throws(() => calculator.received().isEnabled, { instanceOf: SubstituteException }) +}) + +test('not setting a property correctly asserts the call count', t => { + const calculator = Substitute.for() + + calculator.didNotReceive().isEnabled = true + t.throws(() => calculator.received(1).isEnabled = true, { instanceOf: SubstituteException }) + t.throws(() => calculator.received().isEnabled = true, { instanceOf: SubstituteException }) +}) + +test('not calling a method with mock correctly asserts the call count', t => { + const calculator = Substitute.for() + calculator.add(1, 1).returns(2) + + calculator.didNotReceive().add(1, 1) + t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received().add(Arg.all()), { instanceOf: SubstituteException }) +}) + +test('not getting a property with mock correctly asserts the call count', t => { + const calculator = Substitute.for() + calculator.isEnabled.returns(true) + + calculator.didNotReceive().isEnabled + t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException }) + t.throws(() => calculator.received().isEnabled, { instanceOf: SubstituteException }) +}) \ No newline at end of file diff --git a/spec/regression/index.test.ts b/spec/regression/index.test.ts new file mode 100644 index 0000000..1ea0105 --- /dev/null +++ b/spec/regression/index.test.ts @@ -0,0 +1,155 @@ +import test from 'ava' + +import { Substitute, Arg } from '../../src' +import { OmitProxyMethods, ObjectSubstitute } from '../../src/Transformations' + +class Dummy { + +} + +export class Example { + a = '1337'; + + c(arg1: string, arg2: string) { + return 'hello ' + arg1 + ' world (' + arg2 + ')' + } + + get d() { + return 1337 + } + + set v(x: string | null | undefined) { + } + + received(stuff: number | string) { + + } + + returnPromise() { + return Promise.resolve(new Dummy()) + } + + foo(): string | undefined | null { + return 'stuff' + } + + bar(a: number, b?: number): number { + return a + b || 0 + } +} + +let instance: Example +let substitute: ObjectSubstitute, Example> + +function initialize() { + instance = new Example() + substitute = Substitute.for() +}; + +const textModifierRegex = /\x1b\[\d+m/g + +test('class with method called \'received\' can be used for call count verification when proxies are suspended', t => { + initialize() + + Substitute.disableFor(substitute).received(2) + + t.throws(() => substitute.received(2).received(2)) + t.notThrows(() => substitute.received(1).received(2)) +}) + +test('class with method called \'received\' can be used for call count verification', t => { + initialize() + + Substitute.disableFor(substitute).received('foo') + + t.notThrows(() => substitute.received(1).received('foo')) + t.throws(() => substitute.received(2).received('foo')) +}) + +test('class string field set received', t => { + initialize() + + substitute.v = undefined + substitute.v = null + substitute.v = 'hello' + substitute.v = 'hello' + substitute.v = 'world' + + t.notThrows(() => substitute.received().v = 'hello') + t.notThrows(() => substitute.received(5).v = Arg.any()) + t.notThrows(() => substitute.received().v = Arg.any()) + t.notThrows(() => substitute.received(2).v = 'hello') + t.notThrows(() => substitute.received(2).v = Arg.is(x => x && x.indexOf('ll') > -1)) + + t.throws(() => substitute.received(2).v = Arg.any()) + t.throws(() => substitute.received(1).v = Arg.any()) + t.throws(() => substitute.received(1).v = Arg.is(x => x && x.indexOf('ll') > -1)) + t.throws(() => substitute.received(3).v = 'hello') +}) + +test('resolving promises works', async t => { + initialize() + + substitute.returnPromise().resolves(1338) + + t.is(1338, await substitute.returnPromise()) +}) + +test('class void returns', t => { + initialize() + + substitute.foo().returns(void 0, null) + + t.is(substitute.foo(), void 0) + t.is(substitute.foo(), null) +}) + +test('class method received', t => { + initialize() + + void substitute.c('hi', 'there') + void substitute.c('hi', 'the1re') + void substitute.c('hi', 'there') + void substitute.c('hi', 'there') + void substitute.c('hi', 'there') + + t.notThrows(() => substitute.received(4).c('hi', 'there')) + t.notThrows(() => substitute.received(1).c('hi', 'the1re')) + t.notThrows(() => substitute.received().c('hi', 'there')) + + const expectedMessage = 'Expected 7 calls to the method c with arguments [\'hi\', \'there\'], but received 4 of such calls.\n' + + 'All calls received to method c:\n' + + '-> call with arguments [\'hi\', \'there\']\n' + + '-> call with arguments [\'hi\', \'the1re\']\n' + + '-> call with arguments [\'hi\', \'there\']\n' + + '-> call with arguments [\'hi\', \'there\']\n' + + '-> call with arguments [\'hi\', \'there\']' + const { message } = t.throws(() => { substitute.received(7).c('hi', 'there') }) + t.is(message.replace(textModifierRegex, ''), expectedMessage) +}) + +test('received call matches after partial mocks using property instance mimicks', t => { + initialize() + + substitute.d.mimicks(() => instance.d) + substitute.c('lala', 'bar') + + substitute.received(1).c('lala', 'bar') + substitute.received(1).c('lala', 'bar') + + t.notThrows(() => substitute.received(1).c('lala', 'bar')) + const expectedMessage = 'Expected 2 calls to the method c with arguments [\'lala\', \'bar\'], but received 1 of such calls.\n' + + 'All calls received to method c:\n' + + '-> call with arguments [\'lala\', \'bar\']' + const { message } = t.throws(() => substitute.received(2).c('lala', 'bar')) + t.is(message.replace(textModifierRegex, ''), expectedMessage) + t.deepEqual(substitute.d, 1337) +}) + +test('partial mocks using property instance mimicks', t => { + initialize() + + substitute.d.mimicks(() => instance.d) + + t.deepEqual(substitute.d, 1337) +}) diff --git a/spec/regression/issues/11.test.ts b/spec/regression/issues/11.test.ts new file mode 100644 index 0000000..4bd6d77 --- /dev/null +++ b/spec/regression/issues/11.test.ts @@ -0,0 +1,28 @@ +import test from 'ava' +import { Substitute, Arg } from '../../../src' + +type Addands = { + op1: number + op2: number +} + +class RealCalculator { + add(addands: Addands): number { + return addands.op1 + addands.op2 + } +} + +test('issue 11: arg.is is only called once', async t => { + let mockedCalculator = Substitute.for() + mockedCalculator.add(Arg.any()).returns(4) + + let count = 0 + mockedCalculator.add({ op1: 1, op2: 2 }) + + mockedCalculator.received(1).add(Arg.is(a => { + count++ + return a.op1 === 1 && a.op2 === 2 + })) + + t.is(count, 1) +}) \ No newline at end of file diff --git a/spec/regression/issues/23.test.ts b/spec/regression/issues/23.test.ts new file mode 100644 index 0000000..a26666f --- /dev/null +++ b/spec/regression/issues/23.test.ts @@ -0,0 +1,25 @@ +import test from 'ava' + +import { Substitute, Arg } from '../../../src' + +interface CalculatorInterface { + add(a: number, b: number): number + subtract(a: number, b: number): number + divide(a: number, b: number): number + isEnabled: boolean +} + +test('issue 23: mimick received should not call method', t => { + const mockedCalculator = Substitute.for() + + let calls = 0 + + mockedCalculator.add(Arg.all()).mimicks((a, b) => { + t.deepEqual(++calls, 1, 'mimick called twice') + return a + b + }) + + mockedCalculator.add(1, 1) // ok + + mockedCalculator.received(1).add(1, 1) // not ok, calls mimick func +}) diff --git a/spec/issues/36.test.ts b/spec/regression/issues/36.test.ts similarity index 57% rename from spec/issues/36.test.ts rename to spec/regression/issues/36.test.ts index 36b2216..54449e2 100644 --- a/spec/issues/36.test.ts +++ b/spec/regression/issues/36.test.ts @@ -1,33 +1,33 @@ -import test from 'ava'; +import test from 'ava' -import { Substitute, Arg } from '../../src/index'; +import { Substitute, Arg } from '../../../src' class Key { private constructor(private _value: string) { } static create() { - return new this('123'); + return new this('123') } get value(): string { - return this._value; + return this._value } } class IData { private constructor(private _serverCheck: Date, private _data: number[]) { } static create() { - return new this(new Date(), [1]); + return new this(new Date(), [1]) } set data(newData: number[]) { - this._data = newData; + this._data = newData } get serverCheck(): Date { - return this._serverCheck; + return this._serverCheck } get data(): number[] { - return this._data; + return this._data } } abstract class IFetch { @@ -37,35 +37,35 @@ abstract class IFetch { class Service { constructor(private _database: IFetch) { } public async handle(arg?: Key) { - const updateData = await this.getData(arg); - updateData.data = [100]; - await this._database.storeUpdates(updateData); + const updateData = await this.getData(arg) + updateData.data = [100] + await this._database.storeUpdates(updateData) } private getData(arg?: Key) { - return this._database.getUpdates(arg); + return this._database.getUpdates(arg) } } test('issue 36 - promises returning object with properties', async t => { - const emptyFetch = Substitute.for(); - emptyFetch.getUpdates(Key.create()).returns(Promise.resolve(IData.create())); - const result = await emptyFetch.getUpdates(Key.create()); - t.true(result.serverCheck instanceof Date, 'given date is instanceof Date'); + const emptyFetch = Substitute.for() + emptyFetch.getUpdates(Key.create()).returns(Promise.resolve(IData.create())) + const result = await emptyFetch.getUpdates(Key.create()) + t.true(result.serverCheck instanceof Date, 'given date is instanceof Date') t.deepEqual(result.data, [1], 'arrays are deep equal') -}); +}) test('using objects or classes as arguments should be able to match mock', async t => { - const db = Substitute.for(); - const data = IData.create(); - db.getUpdates(Key.create()).returns(Promise.resolve(data)); - const service = new Service(db); + const db = Substitute.for() + const data = IData.create() + db.getUpdates(Key.create()).returns(Promise.resolve(data)) + const service = new Service(db) - await service.handle(Key.create()); + await service.handle(Key.create()) db.received(1).storeUpdates(Arg.is((arg: IData) => arg.serverCheck instanceof Date && arg instanceof IData && arg.data[0] === 100 - )); - t.pass(); -}); \ No newline at end of file + )) + t.pass() +}) \ No newline at end of file diff --git a/spec/regression/issues/45.test.ts b/spec/regression/issues/45.test.ts new file mode 100644 index 0000000..e77f985 --- /dev/null +++ b/spec/regression/issues/45.test.ts @@ -0,0 +1,36 @@ +import test from 'ava' + +import { Substitute, Arg } from '../../../src' + +class DependencyClass { + public methodOne() { } + public methodTwo(someArg: string) { } +} + +class SubjectClass { + private readonly dependency: DependencyClass + + public constructor(dependency: DependencyClass) { + this.dependency = dependency + } + + public callToMethodOne() { + this.dependency.methodOne() + } + public callToMethodTwo() { + this.dependency.methodTwo('string') + } +} + +test('issue 45 Checking received calls off at times', async t => { + const mock = Substitute.for() + const subject = new SubjectClass(mock) + + subject.callToMethodOne() + subject.callToMethodTwo() + + t.notThrows(() => { + mock.received(1).methodOne() + mock.received(1).methodTwo(Arg.is(x => x === 'string')) + }) +}) diff --git a/spec/issues/59.test.ts b/spec/regression/issues/59.test.ts similarity index 76% rename from spec/issues/59.test.ts rename to spec/regression/issues/59.test.ts index 96edb8e..13ffd81 100644 --- a/spec/issues/59.test.ts +++ b/spec/regression/issues/59.test.ts @@ -1,6 +1,6 @@ -import test from 'ava'; +import test from 'ava' -import { Substitute } from '../../src/index'; +import { Substitute } from '../../../src' interface IEcho { echo(a: string): string @@ -13,6 +13,6 @@ test('issue 59 - Mock function with optional parameters', (t) => { echoer.maybeEcho().returns('baz') t.is(echoer.maybeEcho('foo'), 'bar') - echoer.received().maybeEcho('foo'); + echoer.received().maybeEcho('foo') t.is(echoer.maybeEcho(), 'baz') }) diff --git a/spec/regression/mimicks.spec.ts b/spec/regression/mimicks.spec.ts new file mode 100644 index 0000000..1592e15 --- /dev/null +++ b/spec/regression/mimicks.spec.ts @@ -0,0 +1,65 @@ +import test from 'ava' +import { Substitute, Arg } from '../../src' + +interface Calculator { + add(a: number, b: number): number + multiply(a: number, b?: number): number + clear(): void + getMemory(): Promise + viewResult(back?: number): number + heavyOperation(...input: number[]): Promise + isEnabled: boolean + model: Promise +}; + +test('mimicks a method with specific arguments', t => { + const calculator = Substitute.for() + const addMimick = (a: number, b: number) => a + b + + calculator.add(1, 1).mimicks(addMimick) + t.is(calculator.add(1, 1), 2) +}) + +test('mimicks a method with specific and conditional arguments', t => { + const calculator = Substitute.for() + const addMimick = (a: number, b: number) => a + b + + calculator.add(Arg.any('number'), Arg.is((input: number) => input >= 0 && input <= 10)).mimicks(addMimick) + calculator.add(42, -42).mimicks((a: number, b: number) => 0) + + t.is(calculator.add(1234, 6), 1240) + t.is(calculator.add(42, -42), 0) +}) + +test('mimicks a method with Arg.all', t => { + const calculator = Substitute.for() + const addMimick = (a: number, b: number) => a + b + + + calculator.add(Arg.all()).mimicks(addMimick) + t.is(calculator.add(42, 58), 100) +}) + +test('mimicks a method with optional arguments', t => { + const calculator = Substitute.for() + const multiplyOneArgMimicks = (a: number) => a * a + const multiplyMimicks = (a: number, b?: number) => a * b + + calculator.multiply(0, Arg.is((b: number) => b > 10 && b < 20)).mimicks(multiplyMimicks) + calculator.multiply(Arg.any('number'), Arg.is((b: number) => b === 2)).mimicks(multiplyMimicks) + calculator.multiply(2).mimicks(multiplyOneArgMimicks) + + t.is(calculator.multiply(0, 13), 0) + t.is(calculator.multiply(42, 2), 84) + t.is(calculator.multiply(2), 4) +}) + +test('mimicks a method where it\'s only argument is optional', t => { + const calculator = Substitute.for() + + calculator.viewResult().mimicks(() => 0) + calculator.viewResult(3).mimicks(() => 42) + + t.is(calculator.viewResult(), 0) + t.is(calculator.viewResult(3), 42) +}) \ No newline at end of file diff --git a/spec/regression/received.spec.ts b/spec/regression/received.spec.ts new file mode 100644 index 0000000..3fac2d7 --- /dev/null +++ b/spec/regression/received.spec.ts @@ -0,0 +1,137 @@ +import test from 'ava' +import { Substitute, Arg } from '../../src' +import { SubstituteException } from '../../src/SubstituteException' + +interface Calculator { + add(a: number, b: number): number + multiply(a: number, b?: number): number + isEnabled: boolean +} + +test('calling a method twice correctly asserts the call count', t => { + const calculator = Substitute.for() + calculator.add(1, 1) + calculator.add(1, 1) + + calculator.received(2).add(1, 1) + calculator.received().add(1, 1) + t.throws(() => calculator.received(0).add(1, 1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received(2).add(1, 0), { instanceOf: SubstituteException }) + t.throws(() => calculator.received().add(1, 0), { instanceOf: SubstituteException }) +}) + +test('calling a method twice correctly asserts the call count each time', t => { + const calculator = Substitute.for() + + calculator.add(1, 1) + calculator.received(1).add(1, 1) + + calculator.add(1, 1) + calculator.received(2).add(1, 1) + + t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }) +}) + +test('calling a method with optional arguments correctly asserts the call count', t => { + const calculator = Substitute.for() + calculator.multiply(1) + calculator.multiply(1, 1) + calculator.multiply(2, 1) + + calculator.received(1).multiply(1) + calculator.received(1).multiply(1, 1) + calculator.received(0).multiply(2) + calculator.received(1).multiply(2, 1) + + t.throws(() => calculator.received(0).multiply(1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received(0).multiply(1, 1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received(2).multiply(1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received(2).multiply(1, 1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received(1).multiply(2), { instanceOf: SubstituteException }) +}) + +test('getting a property twice correctly asserts the call count', t => { + const calculator = Substitute.for() + calculator.isEnabled + calculator.isEnabled + + calculator.received(2).isEnabled + calculator.received().isEnabled + t.throws(() => calculator.received(0).isEnabled, { instanceOf: SubstituteException }) + t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException }) +}) + +test('setting a property twice correctly asserts the call count', t => { + const calculator = Substitute.for() + calculator.isEnabled = true + calculator.isEnabled = true + + calculator.received(2).isEnabled = true + calculator.received().isEnabled = true + t.throws(() => calculator.received(0).isEnabled = true, { instanceOf: SubstituteException }) + t.throws(() => calculator.received(1).isEnabled = true, { instanceOf: SubstituteException }) + t.throws(() => calculator.received(2).isEnabled = false, { instanceOf: SubstituteException }) + t.throws(() => calculator.received().isEnabled = false, { instanceOf: SubstituteException }) +}) + +test('calling a method twice with mock correctly asserts the call count', t => { + const calculator = Substitute.for() + calculator.add(1, 1).returns(2) + calculator.add(1, 1) + calculator.add(1, 1) + + calculator.received(2).add(1, 1) + calculator.received().add(1, 1) + t.throws(() => calculator.received(0).add(1, 1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received(2).add(1, 0), { instanceOf: SubstituteException }) + t.throws(() => calculator.received().add(1, 0), { instanceOf: SubstituteException }) +}) + +test('calling a method with mock based on Arg correctly asserts the call count', t => { + const calculator = Substitute.for() + calculator.add(Arg.all()).returns(0) + calculator.add(1, 1) + + calculator.received().add(Arg.all()) + calculator.received().add(Arg.any(), Arg.any()) + calculator.received().add(Arg.any('number'), Arg.any('number')) + calculator.received().add(1, 1) + t.throws(() => calculator.received().add(1, 0), { instanceOf: SubstituteException }) +}) + +test('getting a property twice with mock correctly asserts the call count', t => { + const calculator = Substitute.for() + calculator.isEnabled.returns(true) + calculator.isEnabled + calculator.isEnabled + + calculator.received(2).isEnabled + calculator.received().isEnabled + t.throws(() => calculator.received(0).isEnabled, { instanceOf: SubstituteException }) + t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException }) +}) + +test('calling a method with Arg correctly asserts the call count with Arg', t => { + // #18: receive with arg https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/18 + const calculator = Substitute.for() + calculator.add(Arg.all()) + + calculator.received(1).add(Arg.all()) + calculator.received().add(Arg.all()) + t.throws(() => calculator.received(0).add(1, 1), { instanceOf: SubstituteException }) +}) + +test('calling a method does not interfere with other properties or methods call counts', t => { + // #51: All functions share the same state https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/51 + const calculator = Substitute.for() + calculator.add(1, 1) + + calculator.received(0).multiply(1, 1) + calculator.received(0).multiply(Arg.all()) + calculator.received(0).isEnabled + + t.throws(() => calculator.received().multiply(1, 1), { instanceOf: SubstituteException }) + t.throws(() => calculator.received().multiply(Arg.all()), { instanceOf: SubstituteException }) +}) \ No newline at end of file diff --git a/spec/regression/rejects.spec.ts b/spec/regression/rejects.spec.ts new file mode 100644 index 0000000..a92cdbf --- /dev/null +++ b/spec/regression/rejects.spec.ts @@ -0,0 +1,48 @@ +import test from 'ava' + +import { Substitute, Arg } from '../../src' + +interface Calculator { + getMemory(): Promise + heavyOperation(...args: number[]): Promise + model: Promise +} + +test('rejects a method with no arguments', async t => { + const calculator = Substitute.for() + calculator.getMemory().rejects(new Error('No memory')) + + await t.throwsAsync(calculator.getMemory(), { instanceOf: Error, message: 'No memory' }) +}) + +test('rejects a method with arguments', async t => { + const calculator = Substitute.for() + calculator.heavyOperation(0, 1, 1, 2, 4, 5, 8).rejects(new Error('Wrong sequence!')) + + await t.throwsAsync(calculator.heavyOperation(0, 1, 1, 2, 4, 5, 8), { instanceOf: Error, message: 'Wrong sequence!' }) +}) + +test('rejects different values in the specified order on a method', async t => { + const calculator = Substitute.for() + calculator.heavyOperation(Arg.any('number')).rejects(new Error('Wrong!'), new Error('Wrong again!')) + + await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong!' }) + await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong again!' }) + await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong again!' }) +}) + +test('rejects a property', async t => { + const calculator = Substitute.for() + calculator.model.rejects(new Error('No model')) + + await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' }) +}) + +test('rejects different values in the specified order on a property', async t => { + const calculator = Substitute.for() + calculator.model.rejects(new Error('No model'), new Error('I said "no model"')) + + await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' }) + await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'I said "no model"' }) + await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'I said "no model"' }) +}) diff --git a/spec/regression/resolves.spec.ts b/spec/regression/resolves.spec.ts new file mode 100644 index 0000000..6916d1b --- /dev/null +++ b/spec/regression/resolves.spec.ts @@ -0,0 +1,49 @@ +import test from 'ava' + +import { Substitute, Arg } from '../../src' + +interface Calculator { + getMemory(): Promise + heavyOperation(...args: number[]): Promise + model: Promise +} + +test('resolves a method with no arguments', async t => { + const calculator = Substitute.for() + calculator.getMemory().resolves(0) + + t.is(await calculator.getMemory(), 0) +}) + +test('resolves a method with arguments', async t => { + const calculator = Substitute.for() + calculator.heavyOperation(0, 1, 1, 2, 3, 5, 8).resolves(13) + + t.is(await calculator.heavyOperation(0, 1, 1, 2, 3, 5, 8), 13) +}) + +test('resolves different values in the specified order on a method', async t => { + const calculator = Substitute.for() + calculator.heavyOperation(Arg.any('number')).resolves(1, 2, 3) + + t.is(await calculator.heavyOperation(0), 1) + t.is(await calculator.heavyOperation(0), 2) + t.is(await calculator.heavyOperation(0), 3) + t.is(await calculator.heavyOperation(0), 3) // https://github.com/nsubstitute/NSubstitute/blob/master/tests/NSubstitute.Acceptance.Specs/ReturningResults.cs#L37-L42 +}) + +test('resolves a property', async t => { + const calculator = Substitute.for() + calculator.model.resolves('Casio FX-82') + + t.is(await calculator.model, 'Casio FX-82') +}) + +test('resolves different values in the specified order on a property', async t => { + const calculator = Substitute.for() + calculator.model.resolves('Casio FX-82', 'TI-84 Plus') + + t.is(await calculator.model, 'Casio FX-82') + t.is(await calculator.model, 'TI-84 Plus') + t.is(await calculator.model, 'TI-84 Plus') +}) diff --git a/spec/regression/returns.spec.ts b/spec/regression/returns.spec.ts new file mode 100644 index 0000000..ef6cdbb --- /dev/null +++ b/spec/regression/returns.spec.ts @@ -0,0 +1,184 @@ +import test from 'ava' +import { types } from 'util' + +import { Substitute, Arg } from '../../src' + +interface Calculator { + add(a: number, b: number): number + multiply(a: number, b?: number): number + clear(): void + getMemory(): Promise + viewResult(back?: number): number + heavyOperation(...input: number[]): Promise + isEnabled: boolean + model: Promise +}; + +test('returns a primitive value for method with no arguments', t => { + const calculator = Substitute.for() + calculator.clear().returns() + + t.is(calculator.clear(), void 0) +}) + +test('returns a primitive value for method with specific arguments', t => { + const calculator = Substitute.for() + const noResult = calculator.add(1, 1) + + calculator.add(1, 1).returns(2) + + t.is(calculator.add(1, 1), 2) + t.is(calculator.add(1, 1), 2) + t.true(types.isProxy(noResult)) +}) + +test('returns a primitive value for method with specific arguments where the last argument is optional', t => { + const calculator = Substitute.for() + + calculator.multiply(2).returns(4) + calculator.multiply(0, Arg.any('number')).returns(0) + calculator.multiply(1, Arg.any()).returns(10) + calculator.multiply(1).returns(1) + + t.is(calculator.multiply(2), 4) + t.is(calculator.multiply(0, 10), 0) + t.is(calculator.multiply(1), 1) + t.is(calculator.multiply(1, 10), 10) + + const noResult = calculator.multiply(2, 2) + const noResult2 = calculator.multiply(0) + + t.true(types.isProxy(noResult)) + t.true(types.isProxy(noResult2)) +}) + +test('returns a primitive value for method with specific and conditional arguments', t => { + const calculator = Substitute.for() + calculator.add(0, 0).returns(0) + calculator.add(1, Arg.is((input: number) => input === 1)).returns(2) + calculator.add(2, Arg.any('number')).returns(10) + calculator.add(Arg.is((input: number) => input > 2), Arg.any('number')).returns(42) + + const results = [calculator.add(0, 0), calculator.add(1, 1), calculator.add(2, 100), calculator.add(42, 84)] + + t.deepEqual(results, [0, 2, 10, 42]) +}) + +test('returns a primitive value for method with Arg.all', t => { + // #25: call verification does not work when using Arg.all() to set up return values https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/25 + const calculator = Substitute.for() + calculator.add(Arg.all()).returns(42) + + const results = [calculator.add(0, 0), calculator.add(1, 1), calculator.add(2, 100)] + + t.deepEqual(results, [42, 42, 42]) +}) + +test('returns a primitive value for method with one optional argument', t => { + // #24: Mocked method arguments not allowed when verifying method was called https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/24 + const calculator = Substitute.for() + calculator.viewResult().returns(0) + calculator.viewResult(3).returns(123) + + t.is(calculator.viewResult(), 0) + t.is(calculator.viewResult(3), 123) +}) + +test('returns a promise for method with no arguments', async t => { + const calculator = Substitute.for() + calculator.getMemory().returns(Promise.resolve(42)) + + t.is(await calculator.getMemory(), 42) +}) + +test('returns a promise for method with specific arguments', async t => { + const calculator = Substitute.for() + calculator.heavyOperation(1, 1).returns(Promise.resolve(true)) + + const result = await calculator.heavyOperation(1, 1) + const noResult = calculator.heavyOperation(1, 1, 1) + + t.true(result) + t.true(types.isProxy(noResult)) +}) + +test('returns a promise for method with specific and conditional arguments', async t => { + const calculator = Substitute.for() + calculator.heavyOperation(0).returns(Promise.resolve(true)) + calculator.heavyOperation(1, Arg.is((input: number) => input === 1)).returns(Promise.resolve(false)) + calculator.heavyOperation(2, Arg.any('number'), 100).returns(Promise.resolve(true)) + + const results = await Promise.all([calculator.heavyOperation(0), calculator.heavyOperation(1, 1), calculator.heavyOperation(2, 4321, 100)]) + + t.deepEqual(results, [true, false, true]) +}) + +test('returns a promise for method with Arg.all', async t => { + const calculator = Substitute.for() + calculator.heavyOperation(Arg.all()).returns(Promise.resolve(true)) + + const results = await Promise.all([calculator.heavyOperation(0), calculator.heavyOperation(4321, 11, 42, 1234), calculator.heavyOperation(-1, 444)]) + + t.deepEqual(results, [true, true, true]) +}) + +test('returns different primitive values in the specified order for method with arguments', t => { + const calculator = Substitute.for() + calculator.add(1, Arg.any()).returns(1, NaN) + + t.is(calculator.add(1, 1), 1) + t.is(calculator.add(1, 0), NaN) + t.is(calculator.add(1, 1), NaN) + t.is(calculator.add(1, 5), NaN) +}) + +test('returns another substituted instance for method with arguments', t => { + const calculator = Substitute.for() + const addResult = Substitute.for() + addResult.toLocaleString().returns('What a weird number') + calculator.add(1, Arg.any()).returns(addResult) + + const result = calculator.add(1, 1) + + t.is(result, addResult) + t.is(result.toLocaleString(), 'What a weird number') +}) + +test('returns a primitive value on a property', t => { + // #15: can call properties twice https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/15 + const calculator = Substitute.for() + const noResult = calculator.isEnabled + + calculator.isEnabled.returns(true) + + t.true(calculator.isEnabled) + t.true(calculator.isEnabled) + t.true(types.isProxy(noResult)) +}) + +test('returns a promise on a property', async t => { + const calculator = Substitute.for() + calculator.model.returns(Promise.resolve('Casio FX-82')) + + t.is(await calculator.model, 'Casio FX-82') +}) + +test('returns different primitive values in the specified order on a property', t => { + const calculator = Substitute.for() + calculator.isEnabled.returns(false, true) + + t.is(calculator.isEnabled, false) + t.is(calculator.isEnabled, true) + t.is(calculator.isEnabled, true) +}) + +test('returns another substituted instance on a property', async t => { + const calculator = Substitute.for() + const modelResult = Substitute.for() + modelResult.replace(Arg.all()).returns('TI-83') + calculator.model.returns(Promise.resolve(modelResult)) + + const result = await calculator.model + t.is(result, modelResult) + t.is(result.replace('...', '---'), 'TI-83') +}) \ No newline at end of file diff --git a/spec/throws.spec.ts b/spec/regression/throws.spec.ts similarity index 97% rename from spec/throws.spec.ts rename to spec/regression/throws.spec.ts index d754bb9..80d3c21 100644 --- a/spec/throws.spec.ts +++ b/spec/regression/throws.spec.ts @@ -1,6 +1,6 @@ import test from 'ava' -import { Substitute, Arg } from '../src' +import { Substitute, Arg } from '../../src' interface Calculator { add(a: number, b: number): number diff --git a/spec/rejects.spec.ts b/spec/rejects.spec.ts deleted file mode 100644 index 8aa1db2..0000000 --- a/spec/rejects.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import test from 'ava'; - -import { Substitute, Arg } from '../src'; - -interface Calculator { - getMemory(): Promise; - heavyOperation(...args: number[]): Promise; - model: Promise; -} - -test('rejects a method with no arguments', async t => { - const calculator = Substitute.for(); - calculator.getMemory().rejects(new Error('No memory')); - - await t.throwsAsync(calculator.getMemory(), { instanceOf: Error, message: 'No memory' }); -}); - -test('rejects a method with arguments', async t => { - const calculator = Substitute.for(); - calculator.heavyOperation(0, 1, 1, 2, 4, 5, 8).rejects(new Error('Wrong sequence!')); - - await t.throwsAsync(calculator.heavyOperation(0, 1, 1, 2, 4, 5, 8), { instanceOf: Error, message: 'Wrong sequence!' }); -}); - -test('rejects different values in the specified order on a method', async t => { - const calculator = Substitute.for(); - calculator.heavyOperation(Arg.any('number')).rejects(new Error('Wrong!'), new Error('Wrong again!')); - - await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong!' }); - await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong again!' }); - await calculator.heavyOperation(0) - .then(() => t.fail('Promise.catch should have been executed')) - .catch(error => t.is(error, void 0)); -}); - -test('rejects a property', async t => { - const calculator = Substitute.for(); - calculator.model.rejects(new Error('No model')); - - await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' }); -}); - -test('rejects different values in the specified order on a property', async t => { - const calculator = Substitute.for(); - calculator.model.rejects(new Error('No model'), new Error('I said "no model"')); - - await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' }); - await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'I said "no model"' }); - await calculator.model - .then(() => t.fail('Promise.catch should have been executed')) - .catch(error => t.is(error, void 0)); -}); diff --git a/spec/resolves.spec.ts b/spec/resolves.spec.ts deleted file mode 100644 index aaf5bb7..0000000 --- a/spec/resolves.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import test from 'ava'; - -import { Substitute, Arg } from '../src'; - -interface Calculator { - getMemory(): Promise; - heavyOperation(...args: number[]): Promise; - model: Promise; -} - -test('resolves a method with no arguments', async t => { - const calculator = Substitute.for(); - calculator.getMemory().resolves(0); - - t.is(await calculator.getMemory(), 0); -}); - -test('resolves a method with arguments', async t => { - const calculator = Substitute.for(); - calculator.heavyOperation(0, 1, 1, 2, 3, 5, 8).resolves(13); - - t.is(await calculator.heavyOperation(0, 1, 1, 2, 3, 5, 8), 13); -}); - -test('resolves different values in the specified order on a method', async t => { - const calculator = Substitute.for(); - calculator.heavyOperation(Arg.any('number')).resolves(1, 2, 3); - - t.is(await calculator.heavyOperation(0), 1); - t.is(await calculator.heavyOperation(0), 2); - t.is(await calculator.heavyOperation(0), 3); - t.is(await calculator.heavyOperation(0), void 0); -}); - -test('resolves a property', async t => { - const calculator = Substitute.for(); - calculator.model.resolves('Casio FX-82'); - - t.is(await calculator.model, 'Casio FX-82'); -}); - -test('resolves different values in the specified order on a property', async t => { - const calculator = Substitute.for(); - calculator.model.resolves('Casio FX-82', 'TI-84 Plus'); - - t.is(await calculator.model, 'Casio FX-82'); - t.is(await calculator.model, 'TI-84 Plus'); - t.is(await calculator.model, void 0); -}); diff --git a/spec/returns.spec.ts b/spec/returns.spec.ts deleted file mode 100644 index 0cce01e..0000000 --- a/spec/returns.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -import test from 'ava'; -import { inspect } from 'util' - -import { Substitute, Arg } from '../src'; -import { getCorrectConstructorDescriptor } from './util/compatibility'; - -interface Calculator { - add(a: number, b: number): number; - multiply(a: number, b?: number): number; - clear(): void; - getMemory(): Promise; - viewResult(back?: number): number; - heavyOperation(...input: number[]): Promise; - isEnabled: boolean; - model: Promise; -}; - -test('returns a primitive value for method with no arguments', t => { - const calculator = Substitute.for(); - calculator.clear().returns(); - - t.is(calculator.clear(), void 0); -}); - -test('returns a primitive value for method with specific arguments', t => { - const calculator = Substitute.for(); - const noResult = calculator.add(1, 1); - - calculator.add(1, 1).returns(2); - - t.is(calculator.add(1, 1), 2); - t.is(calculator.add(1, 1), 2); - t.is(inspect(noResult.constructor), `[${getCorrectConstructorDescriptor()} SubstituteJS]`); -}); - -test('returns a primitive value for method with specific arguments where the last argument is optional', t => { - const calculator = Substitute.for(); - - calculator.multiply(2).returns(4); - calculator.multiply(0, Arg.any('number')).returns(0); - calculator.multiply(1, Arg.any()).returns(10); - - t.is(calculator.multiply(2), 4); - t.is(calculator.multiply(0, 10), 0); - t.is(calculator.multiply(1), 10); - t.is(calculator.multiply(1, 10), 10); - - const noResult = calculator.multiply(2, 2); - const noResult2 = calculator.multiply(0); - - t.is(inspect(noResult.constructor), `[${getCorrectConstructorDescriptor()} SubstituteJS]`); - t.is(inspect(noResult2.constructor), `[${getCorrectConstructorDescriptor()} SubstituteJS]`); -}); - -test('returns a primitive value for method with specific and conditional arguments', t => { - const calculator = Substitute.for(); - calculator.add(0, 0).returns(0); - calculator.add(1, Arg.is((input: number) => input === 1)).returns(2); - calculator.add(2, Arg.any('number')).returns(10); - calculator.add(Arg.is((input: number) => input > 2), Arg.any('number')).returns(42); - - const results = [calculator.add(0, 0), calculator.add(1, 1), calculator.add(2, 100), calculator.add(42, 84)]; - - t.deepEqual(results, [0, 2, 10, 42]); -}); - -test('returns a primitive value for method with Arg.all', t => { - // #25: call verification does not work when using Arg.all() to set up return values https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/25 - const calculator = Substitute.for(); - calculator.add(Arg.all()).returns(42); - - const results = [calculator.add(0, 0), calculator.add(1, 1), calculator.add(2, 100)]; - - t.deepEqual(results, [42, 42, 42]); -}); - -test('returns a primitive value for method with one optional argument', t => { - // #24: Mocked method arguments not allowed when verifying method was called https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/24 - const calculator = Substitute.for(); - calculator.viewResult().returns(0); - calculator.viewResult(3).returns(123); - - t.is(calculator.viewResult(), 0); - t.is(calculator.viewResult(3), 123); -}); - -test('returns a promise for method with no arguments', async t => { - const calculator = Substitute.for(); - calculator.getMemory().returns(Promise.resolve(42)) - - t.is(await calculator.getMemory(), 42); -}); - -test('returns a promise for method with specific arguments', async t => { - const calculator = Substitute.for(); - calculator.heavyOperation(1, 1).returns(Promise.resolve(true)); - - const result = await calculator.heavyOperation(1, 1); - const noResult = calculator.heavyOperation(1, 1, 1); - - t.is(result, true); - t.is(inspect(noResult.constructor), `[${getCorrectConstructorDescriptor()} SubstituteJS]`); -}); - -test('returns a promise for method with specific and conditional arguments', async t => { - const calculator = Substitute.for(); - calculator.heavyOperation(0).returns(Promise.resolve(true)); - calculator.heavyOperation(1, Arg.is((input: number) => input === 1)).returns(Promise.resolve(false)); - calculator.heavyOperation(2, Arg.any('number'), 100).returns(Promise.resolve(true)); - - const results = await Promise.all([calculator.heavyOperation(0), calculator.heavyOperation(1, 1), calculator.heavyOperation(2, 4321, 100)]); - - t.deepEqual(results, [true, false, true]); -}); - -test('returns a promise for method with Arg.all', async t => { - const calculator = Substitute.for(); - calculator.heavyOperation(Arg.all()).returns(Promise.resolve(true)); - - const results = await Promise.all([calculator.heavyOperation(0), calculator.heavyOperation(4321, 11, 42, 1234), calculator.heavyOperation(-1, 444)]); - - t.deepEqual(results, [true, true, true]); -}); - -test('returns different primitive values in the specified order for method with arguments', t => { - const calculator = Substitute.for(); - calculator.add(1, Arg.any()).returns(1, NaN); - - t.is(calculator.add(1, 1), 1); - t.is(calculator.add(1, 0), NaN); - t.is(calculator.add(1, 1), void 0); - t.is(calculator.add(1, 5), void 0); -}); - -test('returns another substituted instance for method with arguments', t => { - const calculator = Substitute.for(); - const addResult = Substitute.for(); - addResult.toLocaleString().returns('What a weird number'); - calculator.add(1, Arg.any()).returns(addResult); - - const result = calculator.add(1, 1); - - t.is(result, addResult); - t.is(result.toLocaleString(), 'What a weird number'); -}); - -test('returns a primitive value on a property', t => { - // #15: can call properties twice https://github.com/ffMathy/FluffySpoon.JavaScript.Testing.Faking/issues/15 - const calculator = Substitute.for(); - const noResult = calculator.isEnabled; - - calculator.isEnabled.returns(true); - - t.is(calculator.isEnabled, true); - t.is(calculator.isEnabled, true); - t.is(inspect(noResult.constructor), `[${getCorrectConstructorDescriptor()} SubstituteJS]`); -}); - -test('returns a promise on a property', async t => { - const calculator = Substitute.for(); - calculator.model.returns(Promise.resolve('Casio FX-82')); - - t.is(await calculator.model, 'Casio FX-82'); -}); - -test('returns different primitive values in the specified order on a property', t => { - const calculator = Substitute.for(); - calculator.isEnabled.returns(false, true); - - t.is(calculator.isEnabled, false); - t.is(calculator.isEnabled, true); - t.is(calculator.isEnabled, void 0); - t.is(calculator.isEnabled, void 0); -}); - -test('returns another substituted instance on a property', async t => { - const calculator = Substitute.for(); - const modelResult = Substitute.for(); - modelResult.replace(Arg.all()).returns('TI-83'); - calculator.model.returns(Promise.resolve(modelResult)); - - const result = await calculator.model; - - t.is(result, modelResult); - t.is(result.replace('...', '---'), 'TI-83'); -}); \ No newline at end of file diff --git a/spec/util/compatibility.ts b/spec/util/compatibility.ts deleted file mode 100644 index ee10811..0000000 --- a/spec/util/compatibility.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const getCorrectConstructorDescriptor = () => { - const nodeVersionLowerThan12 = Number(process.versions.node.split('.')[0]) < 12; - return nodeVersionLowerThan12 ? - 'Function:' : - 'class'; -}; \ No newline at end of file diff --git a/src/Arguments.ts b/src/Arguments.ts index 5d03c33..5b60c02 100644 --- a/src/Arguments.ts +++ b/src/Arguments.ts @@ -1,105 +1,98 @@ type PredicateFunction = (arg: T) => boolean type ArgumentOptions = { - inverseMatch?: boolean + inverseMatch?: boolean } class BaseArgument { - constructor( - private _description: string, - private _matchingFunction: PredicateFunction, - private _options?: ArgumentOptions - ) { } - - matches(arg: T) { - const inverseMatch = this._options?.inverseMatch ?? false - return inverseMatch ? !this._matchingFunction(arg) : this._matchingFunction(arg); - } - - toString() { - return this._description; - } - - [Symbol.for('nodejs.util.inspect.custom')]() { - return this._description; - } + constructor( + private _description: string, + private _matchingFunction: PredicateFunction, + private _options?: ArgumentOptions + ) { } + + matches(arg: T) { + const inverseMatch = this._options?.inverseMatch ?? false + return inverseMatch ? !this._matchingFunction(arg) : this._matchingFunction(arg) + } + + toString() { + return this._description + } + + [Symbol.for('nodejs.util.inspect.custom')]() { + return this._description + } } export class Argument extends BaseArgument { - private readonly _type = 'SingleArgument'; - constructor(description: string, matchingFunction: PredicateFunction, options?: ArgumentOptions) { - super(description, matchingFunction, options); - } - get type(): 'SingleArgument' { - return this._type; - } + private readonly _type = 'SingleArgument'; + get type(): 'SingleArgument' { + return this._type + } } export class AllArguments extends BaseArgument { - private readonly _type = 'AllArguments'; - constructor() { - super('{all}', () => true, {}); - } - get type(): 'AllArguments' { - return this._type; - } + private readonly _type = 'AllArguments'; + constructor() { + super('{all}', () => true, {}) + } + get type(): 'AllArguments' { + return this._type // TODO: Needed? + } } export namespace Arg { - type ExtractFirstArg = T extends AllArguments ? TArgs[0] : T - type ReturnArg = Argument & T; - type Inversable = T & { not: T } - const factory = (factoryF: Function) => (...args: any[]): T => factoryF(...args) - const toStringify = (obj: any) => { - if (typeof obj.inspect === 'function') - return obj.inspect(); - - if (typeof obj.toString === 'function') - return obj.toString(); - - return obj; + type Inversable = T & { not: T } + type ExtractFirstArg = T extends AllArguments ? TArgs[0] : T + type ReturnArg = Argument & T + const createInversable = (target: (arg: TArg, opt?: ArgumentOptions) => TReturn): Inversable<(arg: TArg) => TReturn> => { + const inversable = (arg: TArg) => target(arg) + inversable.not = (arg: TArg) => target(arg, { inverseMatch: true }) + return inversable + } + + const toStringify = (obj: any) => { + if (typeof obj.inspect === 'function') return obj.inspect() + if (typeof obj.toString === 'function') return obj.toString() + return obj + } + + export const all = (): AllArguments => new AllArguments() + + type Is = (predicate: PredicateFunction>) => ReturnArg> + const isFunction = (predicate: PredicateFunction>, options?: ArgumentOptions) => new Argument( + `{predicate ${toStringify(predicate)}}`, predicate, options + ) + export const is = createInversable(isFunction) as Inversable + + type MapAnyReturn = T extends 'any' ? + ReturnArg : T extends 'string' ? + ReturnArg : T extends 'number' ? + ReturnArg : T extends 'boolean' ? + ReturnArg : T extends 'symbol' ? + ReturnArg : T extends 'undefined' ? + ReturnArg : T extends 'object' ? + ReturnArg : T extends 'function' ? + ReturnArg : T extends 'array' ? + ReturnArg : any + + type AnyType = 'string' | 'number' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | 'array' | 'any' + type Any = (type?: T) => MapAnyReturn + + const anyFunction = (type: AnyType = 'any', options?: ArgumentOptions) => { + const description = `{type ${type}}` + const predicate = (x: any) => { + switch (type) { + case 'any': + return true + case 'array': + return Array.isArray(x) + default: + return typeof x === type + } } - export const all = (): AllArguments => new AllArguments(); - - type Is = (predicate: PredicateFunction>) => ReturnArg> - const isFunction = >(predicate: T, options?: ArgumentOptions) => new Argument( - `{predicate ${toStringify(predicate)}}`, predicate, options - ); - - const isArgFunction: Inversable = (predicate) => factory(isFunction)(predicate); - isArgFunction.not = (predicate) => factory(isFunction)(predicate, { inverseMatch: true }); - export const is = isArgFunction - - type MapAnyReturn = T extends 'any' ? - ReturnArg : T extends 'string' ? - ReturnArg : T extends 'number' ? - ReturnArg : T extends 'boolean' ? - ReturnArg : T extends 'symbol' ? - ReturnArg : T extends 'undefined' ? - ReturnArg : T extends 'object' ? - ReturnArg : T extends 'function' ? - ReturnArg : T extends 'array' ? - ReturnArg : any; - - type AnyType = 'string' | 'number' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | 'array' | 'any'; - type Any = (type?: T) => MapAnyReturn; - - const anyFunction = (type: AnyType = 'any', options?: ArgumentOptions) => { - const description = !type ? '{any arg}' : `{type ${type}}`; - const predicate = (x: any) => { - switch (type) { - case 'any': - return true; - case 'array': - return Array.isArray(x); - default: - return typeof x === type; - } - } - - return new Argument(description, predicate, options); - } + return new Argument(description, predicate, options) + } - const anyArgFunction: Inversable = (type) => factory(anyFunction)(type); - anyArgFunction.not = (type) => factory(anyFunction)(type, { inverseMatch: true }); - export const any = anyArgFunction; + export const any = createInversable(anyFunction) as Inversable } \ No newline at end of file diff --git a/src/Context.ts b/src/Context.ts deleted file mode 100644 index 9c3d415..0000000 --- a/src/Context.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { inspect } from 'util' -import { ContextState } from './states/ContextState'; -import { InitialState } from './states/InitialState'; -import { HandlerKey } from './Substitute'; -import { PropertyType } from './Utilities'; -import { SetPropertyState } from './states/SetPropertyState'; -import { SubstituteJS as SubstituteBase, SubstituteException } from './SubstituteBase' - -export class Context { - private _initialState: InitialState; - - private _proxy: any; - private _rootProxy: any; - private _receivedProxy: any; - - private _getState: ContextState; - private _setState: ContextState; - private _receivedState: ContextState; - - constructor() { - this._initialState = new InitialState(); - this._setState = this._initialState; - this._getState = this._initialState; - - this._proxy = new Proxy(SubstituteBase, { - apply: (_target, _this, args) => this.getStateApply(_target, _this, args), - set: (_target, property, value) => (this.setStateSet(_target, property, value), true), - get: (_target, property) => this._filterAndReturnProperty(_target, property, this.getStateGet) - }); - - this._rootProxy = new Proxy(SubstituteBase, { - apply: (_target, _this, args) => this.initialState.apply(this, args), - set: (_target, property, value) => (this.initialState.set(this, property, value), true), - get: (_target, property) => this._filterAndReturnProperty(_target, property, this.rootGet) - }); - - this._receivedProxy = new Proxy(SubstituteBase, { - apply: (_target, _this, args) => this._receivedState === void 0 ? void 0 : this._receivedState.apply(this, args), - set: (_target, property, value) => (this.setStateSet(_target, property, value), true), - get: (_target, property) => { - const state = this.initialState.getPropertyStates.find(getPropertyState => getPropertyState.property === property); - if (state === void 0) return this.handleNotFoundState(property); - if (!state.isFunctionState) - state.get(this, property); - this._receivedState = state; - return this.receivedProxy; - } - }); - } - - private _filterAndReturnProperty(target: typeof SubstituteBase, property: PropertyKey, getToExecute: ContextState['get']) { - switch (property) { - case 'constructor': - case 'valueOf': - case '$$typeof': - case 'length': - case 'toString': - case 'inspect': - case 'lastRegisteredSubstituteJSMethodOrProperty': - return target.prototype[property]; - case Symbol.toPrimitive: - return target.prototype[Symbol.toPrimitive]; - case inspect.custom: - return target.prototype[inspect.custom]; - case Symbol.iterator: - return target.prototype[Symbol.iterator]; - case Symbol.toStringTag: - return target.prototype[Symbol.toStringTag]; - default: - target.prototype.lastRegisteredSubstituteJSMethodOrProperty = property.toString() - return getToExecute.bind(this)(target as any, property); - } - } - - private handleNotFoundState(property: PropertyKey) { - if (this.initialState.hasExpectations && this.initialState.expectedCount !== null) { - this.initialState.assertCallCountMatchesExpectations([], 0, PropertyType.property, property, []); - return this.receivedProxy; - } - throw SubstituteException.forPropertyNotMocked(property); - } - - rootGet(_target: any, property: PropertyKey) { - return this.initialState.get(this, property); - } - - getStateApply(_target: any, _this: any, args: any[]) { - return this._getState.apply(this, args); - } - - setStateSet(_target: any, property: PropertyKey, value: any) { - return this._setState.set(this, property, value); - } - - getStateGet(_target: any, property: PropertyKey) { - if (property === HandlerKey) { - return this; - } - - return this._getState.get(this, property); - } - - public get proxy() { - return this._proxy; - } - - public get rootProxy() { - return this._rootProxy; - } - - public get receivedProxy() { - return this._receivedProxy; - } - - public get initialState() { - return this._initialState; - } - - public set state(state: ContextState) { - if (this._setState === state) - return; - - state instanceof SetPropertyState ? - this._setState = state : this._getState = state - if (state.onSwitchedTo) - state.onSwitchedTo(this); - } -} \ No newline at end of file diff --git a/src/RecordedArguments.ts b/src/RecordedArguments.ts new file mode 100644 index 0000000..b778f62 --- /dev/null +++ b/src/RecordedArguments.ts @@ -0,0 +1,93 @@ +import { inspect, InspectOptions, isDeepStrictEqual } from 'util' +import { Argument, AllArguments } from './Arguments' + +type ArgumentsClass = 'plain' | 'with-predicate' | 'wildcard' +export class RecordedArguments { + private _argumentsClass: ArgumentsClass + private _value?: any[] + private readonly _hasNoArguments: boolean = false + + private constructor(rawArguments: any[] | void) { + if (typeof rawArguments === 'undefined') { + this._hasNoArguments = true + return this + } + + this._argumentsClass = this.classifyArguments(rawArguments) + this._value = rawArguments + } + + static from(rawArguments: any[]): RecordedArguments { + return new this(rawArguments) + } + + static none(): RecordedArguments { + return new this() + } + + static sort(objectWithArguments: T[]): T[] { + return objectWithArguments.sort((a, b) => { + const aClass = a.recordedArguments.argumentsClass + const bClass = b.recordedArguments.argumentsClass + + if (aClass === bClass) return 0 + if (aClass === 'plain') return -1 + if (bClass === 'plain') return 1 + + if (aClass === 'with-predicate') return -1 + if (bClass === 'with-predicate') return 1 + + if (aClass === 'wildcard') return -1 + return 1 + }) + } + + get argumentsClass(): ArgumentsClass { + return this._argumentsClass + } + + get value(): any[] | undefined { + return this._value + } + + get hasNoArguments(): boolean { + return this._hasNoArguments + } + + public match(other: RecordedArguments) { + if (this.argumentsClass === 'wildcard' || other.argumentsClass === 'wildcard') return true + if (this.hasNoArguments || other.hasNoArguments) return this.hasNoArguments && other.hasNoArguments + if (this.value.length !== other.value.length) return false + + return this.value.every((argument, index) => this.areArgumentsEqual(argument, other.value[index])) + } + + private classifyArguments(rawArguments: any[]): ArgumentsClass { + const allPlain = rawArguments.every(arg => !(arg instanceof Argument || arg instanceof AllArguments)) + if (allPlain) return 'plain' + + const hasSingleArgument = rawArguments.some(arg => arg instanceof Argument) + if (hasSingleArgument) return 'with-predicate' + + return 'wildcard' + } + + private areArgumentsEqual(a: any, b: any): boolean { + if (a instanceof Argument && b instanceof Argument) return false + if (a instanceof Argument) return a.matches(b) + if (b instanceof Argument) return b.matches(a) + return isDeepStrictEqual(a, b) + } + + [inspect.custom](_: number, options: InspectOptions) { + return this.printableForm(options) + } + + private printableForm(options: InspectOptions): string { + const inspectedValues = this.value?.map(v => inspect(v, options)) + if (!Array.isArray(inspectedValues)) return '' + return inspectedValues.length !== 1 + ? `(${inspectedValues.join(', ')})` + : inspectedValues[0] + } +} \ No newline at end of file diff --git a/src/Recorder.ts b/src/Recorder.ts new file mode 100644 index 0000000..6da2ee8 --- /dev/null +++ b/src/Recorder.ts @@ -0,0 +1,54 @@ +import { inspect, InspectOptions } from 'util' +import { SubstituteNodeBase } from './SubstituteNodeBase' +import { RecordsSet } from './RecordsSet' + +export class Recorder { + private _records: RecordsSet + private _indexedRecords: Map> + + constructor() { + this._records = new RecordsSet() + this._indexedRecords = new Map() + } + + public get records(): RecordsSet { + return this._records + } + + public get indexedRecords(): Map> { + return this._indexedRecords + } + + public addIndexedRecord(node: SubstituteNodeBase): void { + this.addRecord(node) + const existingNodes = this.indexedRecords.get(node.key) + if (typeof existingNodes === 'undefined') this.indexedRecords.set(node.key, new RecordsSet([node])) + else existingNodes.add(node) + } + + public addRecord(node: SubstituteNodeBase): void { + this._records.add(node) + } + + public getSiblingsOf(node: SubstituteNodeBase): RecordsSet { + const siblingNodes = this.indexedRecords.get(node.key) ?? new RecordsSet() + return siblingNodes.filter(siblingNode => siblingNode !== node) + } + + public clearRecords(filterFunction: (node: SubstituteNodeBase) => boolean) { + const recordsToRemove = this.records.filter(filterFunction) + for (const record of recordsToRemove) { + const indexedRecord = this.indexedRecords.get(record.key) + indexedRecord.delete(record) + if (indexedRecord.size === 0) this.indexedRecords.delete(record.key) + this.records.delete(record) + } + } + + public [inspect.custom](_: number, options: InspectOptions): string { + const entries = [...this.indexedRecords.entries()] + return entries.map( + ([key, value]) => `\n ${key.toString()}: {\n${[...value.map(v => ` ${inspect(v, options)}`)].join(',\n')}\n }` + ).join() + } +} diff --git a/src/RecordsSet.ts b/src/RecordsSet.ts new file mode 100644 index 0000000..0d16501 --- /dev/null +++ b/src/RecordsSet.ts @@ -0,0 +1,65 @@ +type FilterFunction = (item: T) => boolean +type MapperFunction = (item: T) => R +type Transformer = { type: 'filter', fnc: FilterFunction } | { type: 'mapper', fnc: MapperFunction } + +export class RecordsSet extends Set { + private _transformer: Transformer + private readonly _prevIter?: RecordsSet + + constructor(value?: Iterable | readonly T[]) { + super(value instanceof RecordsSet ? undefined : value) + if (value instanceof RecordsSet) this._prevIter = value + } + + get size(): number { + const currentSize = super.size + if (this._prevIter instanceof RecordsSet) return currentSize + this._prevIter.size + return currentSize + } + + filter(predicate: (item: T) => boolean): RecordsSet { + const newInstance = new RecordsSet(this) + newInstance._transformer = { type: 'filter', fnc: predicate } + return newInstance + } + + map(predicate: (item: T) => R): RecordsSet { + const newInstance = new RecordsSet(this) + newInstance._transformer = { type: 'mapper', fnc: predicate } + return newInstance as RecordsSet + } + + has(value: T): boolean { + if (super.has(value)) return true + return this._prevIter instanceof RecordsSet ? this._prevIter.has(value) : false + } + + delete(value: T): boolean { + const deleted = super.delete(value) + if (deleted) return true + return this._prevIter instanceof RecordsSet ? this._prevIter.delete(value) : false + } + + clear() { + Object.defineProperty(this, '_prevIter', { value: undefined }) + super.clear() + } + + *[Symbol.iterator](): IterableIterator { + yield* this.values() + } + + *values(): IterableIterator { + if (this._prevIter instanceof RecordsSet) yield* this.applyTransform(this._prevIter) + yield* this.applyTransform(super.values()) + } + + private *applyTransform(itarable: Iterable): IterableIterator { + const transform = this._transformer + if (typeof transform === 'undefined') return yield* itarable + for (const value of itarable) { + if (transform.type === 'mapper') yield transform.fnc(value) + if (transform.type === 'filter' && transform.fnc(value)) yield value + } + } +} \ No newline at end of file diff --git a/src/Substitute.ts b/src/Substitute.ts index cc1ea08..21afa3f 100644 --- a/src/Substitute.ts +++ b/src/Substitute.ts @@ -1,41 +1,81 @@ -import { Context } from './Context'; -import { ObjectSubstitute, OmitProxyMethods, DisabledSubstituteObject } from './Transformations'; - -export const HandlerKey = Symbol(); -export const AreProxiesDisabledKey = Symbol(); - -export type SubstituteOf = ObjectSubstitute, T> & T; - -export class Substitute { - static for(): SubstituteOf { - const objectContext = new Context(); - return objectContext.rootProxy; - } - - static disableFor>>(substitute: T): DisabledSubstituteObject { - const thisProxy = substitute as any; // rootProxy - const thisExposedProxy = thisProxy[HandlerKey]; // Context - - const disableProxy = (f: K): K => { - return function () { - thisProxy[AreProxiesDisabledKey] = true; - const returnValue = f.call(thisExposedProxy, ...arguments); - thisProxy[AreProxiesDisabledKey] = false; - return returnValue; - } as any; - }; - - return new Proxy(() => { }, { - apply: function (_target, _this, args) { - return disableProxy(thisExposedProxy.getStateApply)(...arguments) - }, - set: function (_target, property, value) { - return disableProxy(thisExposedProxy.setStateSet)(...arguments) - }, - get: function (_target, property) { - thisExposedProxy._initialState.handleGet(thisExposedProxy, property) - return disableProxy(thisExposedProxy.getStateGet)(...arguments) - } - }) as any; - } +import { inspect, InspectOptions } from 'util' + +import { SubstituteBase } from './SubstituteBase' +import { createSubstituteProxy } from './SubstituteProxy' +import { Recorder } from './Recorder' +import { DisabledSubstituteObject, ObjectSubstitute, OmitProxyMethods } from './Transformations' + +export type SubstituteOf = ObjectSubstitute, T> & T +type Instantiable = { [SubstituteBase.instance]?: T } + +export class Substitute extends SubstituteBase { + private _proxy: Substitute + private _recorder: Recorder = new Recorder() + private _context: { disableAssertions: boolean } = { disableAssertions: false } + + constructor() { + super() + this._proxy = createSubstituteProxy( + this, + { + get: (target, _property, _, node) => { + if (target.context.disableAssertions) node.disableAssertions() + } + // apply: (target, _, args, __, proxy) => { + // const rootProperty = proxy.get(target, '()', proxy) TODO: Implement to support callable interfaces + // return Reflect.apply(rootProperty, rootProperty, args) + // } + } + ) + } + + static for(): SubstituteOf { + const substitute = new this() + return substitute.proxy as unknown as SubstituteOf + } + + static disableFor & Instantiable>(substituteProxy: T): DisabledSubstituteObject { + const substitute = substituteProxy[SubstituteBase.instance] + + const disableProxy = < + TParameters extends unknown[], + TReturnType extends unknown + >(reflection: (...args: TParameters) => TReturnType): typeof reflection => (...args) => { + substitute.context.disableAssertions = true + const reflectionResult = reflection(...args) + substitute.context.disableAssertions = false + return reflectionResult + } + + return new Proxy(substitute.proxy, { + get: function (target, property) { + return disableProxy(Reflect.get)(target, property) + }, + set: function (target, property, value) { + return disableProxy(Reflect.set)(target, property, value) + }, + apply: function (target, _, args) { + return disableProxy(Reflect.apply)(target, _, args) + } + }) as DisabledSubstituteObject + } + + public get proxy() { + return this._proxy + } + + public get recorder() { + return this._recorder + } + + public get context() { + return this._context + } + + protected printableForm(_: number, options: InspectOptions): string { + const records = inspect(this.recorder, options) + + const instanceName = 'Substitute' // Substitute + return instanceName + ' {' + records + '\n}' + } } \ No newline at end of file diff --git a/src/SubstituteBase.ts b/src/SubstituteBase.ts index f2ea606..a0eb4c0 100644 --- a/src/SubstituteBase.ts +++ b/src/SubstituteBase.ts @@ -1,73 +1,35 @@ -import { inspect } from 'util'; -import { PropertyType, stringifyArguments, stringifyCalls, Call } from './Utilities'; +import { inspect, InspectOptions, types } from 'util' +import { SubstituteException } from './SubstituteException' -export class SubstituteJS { - private _lastRegisteredSubstituteJSMethodOrProperty: string - set lastRegisteredSubstituteJSMethodOrProperty(value: string) { - this._lastRegisteredSubstituteJSMethodOrProperty = value; +const instance = Symbol('Substitute:Instance') +type SpecialProperty = typeof instance | typeof inspect.custom | 'then' +export abstract class SubstituteBase extends Function { + constructor() { + super() } - get lastRegisteredSubstituteJSMethodOrProperty() { - return typeof this._lastRegisteredSubstituteJSMethodOrProperty === 'undefined' ? 'root' : this._lastRegisteredSubstituteJSMethodOrProperty; - } - [Symbol.toPrimitive]() { - return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`; - } - [Symbol.toStringTag]() { - return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`; - } - [Symbol.iterator]() { - return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`; - } - [inspect.custom]() { - return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`; - } - valueOf() { - return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`; - } - $$typeof() { - return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`; - } - toString() { - return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`; - } - inspect() { - return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`; - } - length = `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`; -} -enum SubstituteExceptionTypes { - CallCountMissMatch = 'CallCountMissMatch', - PropertyNotMocked = 'PropertyNotMocked' -} + static instance: typeof instance = instance -export class SubstituteException extends Error { - type: SubstituteExceptionTypes - constructor(msg: string, exceptionType?: SubstituteExceptionTypes) { - super(msg); - Error.captureStackTrace(this, SubstituteException); - this.name = new.target.name; - this.type = exceptionType + protected isSpecialProperty(property: PropertyKey): property is SpecialProperty { + return property === SubstituteBase.instance || property === inspect.custom || property === 'then' } - static forCallCountMissMatch( - callCount: { expected: number | null, received: number }, - property: { type: PropertyType, value: PropertyKey }, - calls: { expectedArguments: any[], received: Call[] } - ) { - const message = 'Expected ' + (callCount.expected === null ? '1 or more' : callCount.expected) + - ' call' + (callCount.expected === 1 ? '' : 's') + ' to the ' + property.type + ' ' + property.value.toString() + - ' with ' + stringifyArguments(calls.expectedArguments) + ', but received ' + (callCount.received === 0 ? 'none' : callCount.received) + - ' of such call' + (callCount.received === 1 ? '' : 's') + - '.\nAll calls received to ' + property.type + ' ' + property.value.toString() + ':' + stringifyCalls(calls.received); - return new this(message, SubstituteExceptionTypes.CallCountMissMatch); + protected evaluateSpecialProperty(property: SpecialProperty) { + switch (property) { + case SubstituteBase.instance: + return this + case inspect.custom: + return this.printableForm.bind(this) + case 'then': + return + default: + throw SubstituteException.generic(`Evaluation of special property ${property} is not implemented`) + } } - static forPropertyNotMocked(property: PropertyKey) { - return new this(`There is no mock for property: ${String(property)}`, SubstituteExceptionTypes.PropertyNotMocked) - } + protected abstract printableForm(_: number, options: InspectOptions): string - static generic(message: string) { - return new this(message) + public [inspect.custom](...args: [_: number, options: InspectOptions]): string { + return types.isProxy(this) ? this[inspect.custom](...args) : this.printableForm(...args) } } \ No newline at end of file diff --git a/src/SubstituteException.ts b/src/SubstituteException.ts new file mode 100644 index 0000000..d72f01f --- /dev/null +++ b/src/SubstituteException.ts @@ -0,0 +1,52 @@ +import { RecordedArguments } from './RecordedArguments' +import { PropertyType, stringifyArguments, stringifyCalls, textModifier, plurify } from './Utilities' + +enum SubstituteExceptionTypes { + CallCountMissMatch = 'CallCountMissMatch', + PropertyNotMocked = 'PropertyNotMocked' +} + +export class SubstituteException extends Error { + type: SubstituteExceptionTypes + + constructor(msg: string, exceptionType?: SubstituteExceptionTypes) { + super(msg) + Error.captureStackTrace(this, SubstituteException) + this.name = new.target.name + this.type = exceptionType + } + + static forCallCountMissMatch( + count: { expected: number | null, received: number }, + property: { type: PropertyType, value: PropertyKey }, + calls: { expected: RecordedArguments, received: RecordedArguments[] } + ) { + const propertyValue = textModifier.bold(property.value.toString()) + const commonMessage = `Expected ${textModifier.bold( + count.expected === undefined ? '1 or more' : count.expected.toString() + )} ${plurify('call', count.expected)} to the ${textModifier.italic(property.type)} ${propertyValue}` + + const messageForMethods = property.type === PropertyType.method ? ` with ${stringifyArguments(calls.expected)}` : '' // should also apply for setters (instead of methods only) + const receivedMessage = `, but received ${textModifier.bold(count.received < 1 ? 'none' : count.received.toString())} of such calls.` + + const callTrace = calls.received.length > 0 + ? `\nAll calls received to ${textModifier.italic(property.type)} ${propertyValue}:${stringifyCalls(calls.received)}` + : '' + + return new this( + commonMessage + messageForMethods + receivedMessage + callTrace, + SubstituteExceptionTypes.CallCountMissMatch + ) + } + + static forPropertyNotMocked(property: PropertyKey) { + return new this( + `There is no mock for property: ${property.toString()}`, + SubstituteExceptionTypes.PropertyNotMocked + ) + } + + static generic(message: string) { + return new this(message) + } +} \ No newline at end of file diff --git a/src/SubstituteNode.ts b/src/SubstituteNode.ts new file mode 100644 index 0000000..800603a --- /dev/null +++ b/src/SubstituteNode.ts @@ -0,0 +1,206 @@ +import { inspect, InspectOptions } from 'util' + +import { PropertyType, isSubstitutionMethod, isAssertionMethod, AssertionMethod, SubstitutionMethod, textModifier, ConfigurationMethod, isSubstituteMethod } from './Utilities' +import { SubstituteException } from './SubstituteException' +import { RecordedArguments } from './RecordedArguments' +import { SubstituteNodeBase } from './SubstituteNodeBase' +import { SubstituteBase } from './SubstituteBase' +import { createSubstituteProxy } from './SubstituteProxy' +import { ClearType } from './Transformations' + +type SubstituteContext = SubstitutionMethod | AssertionMethod | ConfigurationMethod | 'none' +const clearTypeToFilterMap: Record boolean> = { + all: () => true, + receivedCalls: node => !node.hasContext, + substituteValues: node => node.isSubstitution +} + +export class SubstituteNode extends SubstituteNodeBase { + private _proxy: SubstituteNode + private _propertyType: PropertyType = PropertyType.property + private _accessorType: 'get' | 'set' = 'get' + private _recordedArguments: RecordedArguments = RecordedArguments.none() + + private _context: SubstituteContext = 'none' + private _disabledAssertions: boolean = false + + constructor(property: PropertyKey, parent: SubstituteNode | SubstituteBase) { + super(property, parent) + this._proxy = createSubstituteProxy( + this, + { + get: (node, _, __, nextNode) => { + if (node.isAssertion) nextNode.executeAssertion() + }, + set: (node, _, __, ___, nextNode) => { + if (node.isAssertion) nextNode.executeAssertion() + }, + apply: (node, _, rawArguments) => { + node.handleMethod(rawArguments) + if (node.context === 'clearSubstitute') return node.clear() + return node.parent?.isAssertion ?? false ? node.executeAssertion() : node.read() + } + } + ) + } + + public get proxy() { + return this._proxy + } + + get context(): SubstituteContext { + return this._context + } + + get hasContext(): boolean { + return this.context !== 'none' + } + + get isSubstitution(): boolean { + return isSubstitutionMethod(this.context) + } + + get isAssertion(): boolean { + return isAssertionMethod(this.context) + } + + get property() { + return this.key + } + + get propertyType() { + return this._propertyType + } + + get accessorType() { + return this._accessorType + } + + get recordedArguments() { + return this._recordedArguments + } + + public get disabledAssertions() { + return this._disabledAssertions + } + + public assignContext(context: SubstituteContext): void { + this._context = context + } + + public disableAssertions() { + this._disabledAssertions = true + } + + public read(): SubstituteNode | void | never { + if (this.parent?.isSubstitution ?? false) return + if (this.isAssertion) return this.proxy + + const mostSuitableSubstitution = this.getMostSuitableSubstitution() + return mostSuitableSubstitution instanceof SubstituteNode + ? mostSuitableSubstitution.executeSubstitution(this.recordedArguments) + : this.proxy + } + + public write(value: any) { + this._accessorType = 'set' + this._recordedArguments = RecordedArguments.from([value]) + } + + public clear() { + const clearType: ClearType = this.recordedArguments.value[0] ?? 'all' + const filter = clearTypeToFilterMap[clearType] as (node: SubstituteNodeBase) => boolean + this.root.recorder.clearRecords(filter) + } + + public executeSubstitution(contextArguments: RecordedArguments) { + const substitutionMethod = this.context as SubstitutionMethod + const substitutionValue = this.child.recordedArguments.value.length > 1 + ? this.child.recordedArguments.value.shift() + : this.child.recordedArguments.value[0] + switch (substitutionMethod) { + case 'throws': + throw substitutionValue + case 'mimicks': + const argumentsToApply = this.propertyType === PropertyType.property ? [] : contextArguments.value + return substitutionValue(...argumentsToApply) + case 'resolves': + return Promise.resolve(substitutionValue) + case 'rejects': + return Promise.reject(substitutionValue) + case 'returns': + return substitutionValue + default: + throw SubstituteException.generic(`Substitution method '${substitutionMethod}' not implemented`) + } + } + + public executeAssertion(): void | never { + const siblings = [...this.getAllSiblings().filter(n => !n.hasContext && n.accessorType === this.accessorType)] + if (!this.isIntermediateNode()) throw new Error('Not possible') + + const expectedCount = this.parent.recordedArguments.value[0] ?? undefined + const finiteExpectation = expectedCount !== undefined + if (finiteExpectation && (!Number.isInteger(expectedCount) || expectedCount < 0)) throw new Error('Expected count has to be a positive integer') + + const hasRecordedCalls = siblings.length > 0 + const allRecordedArguments = siblings.map(sibling => sibling.recordedArguments) + + if ( + !hasRecordedCalls && + (!finiteExpectation || expectedCount > 0) + ) throw SubstituteException.forCallCountMissMatch( // Here we don't know here if it's a property or method, so we should throw something more generic + { expected: expectedCount, received: 0 }, + { type: this.propertyType, value: this.property }, + { expected: this.recordedArguments, received: allRecordedArguments } + ) + + if (!hasRecordedCalls || siblings.some(sibling => sibling.propertyType === this.propertyType)) { + const actualCount = allRecordedArguments.filter(r => r.match(this.recordedArguments)).length + const matchedExpectation = (!finiteExpectation && actualCount > 0) || expectedCount === actualCount + + if (!matchedExpectation) throw SubstituteException.forCallCountMissMatch( + { expected: expectedCount, received: actualCount }, + { type: this.propertyType, value: this.property }, + { expected: this.recordedArguments, received: allRecordedArguments } + ) + } + } + + public handleMethod(rawArguments: any[]): void { + this._propertyType = PropertyType.method + this._recordedArguments = RecordedArguments.from(rawArguments) + if (!isSubstituteMethod(this.property)) return + + if (this.isIntermediateNode() && isSubstitutionMethod(this.property)) return this.parent.assignContext(this.property) + if (this.disabledAssertions || !this.isHead()) return + + this.assignContext(this.property) + if (this.context === 'didNotReceive') this._recordedArguments = RecordedArguments.from([0]) + } + + private getMostSuitableSubstitution(): SubstituteNode { + const nodes = this.getAllSiblings().filter(node => node.isSubstitution && + node.propertyType === this.propertyType && + node.recordedArguments.match(this.recordedArguments) + ) + const sortedNodes = RecordedArguments.sort([...nodes]) + return sortedNodes[0] + } + + protected printableForm(_: number, options: InspectOptions): string { + const hasContext = this.hasContext + const args = inspect(this.recordedArguments, options) + const label = this.isSubstitution + ? '=> ' + : this.isAssertion + ? `${this.child.property.toString()}` + : '' + const s = hasContext + ? ` ${label}${inspect(this.child?.recordedArguments, options)}` + : '' + + const printableNode = `${this.propertyType}<${this.property.toString()}>: ${args}${s}` + return hasContext ? textModifier.italic(printableNode) : printableNode + } +} \ No newline at end of file diff --git a/src/SubstituteNodeBase.ts b/src/SubstituteNodeBase.ts new file mode 100644 index 0000000..21c9e4d --- /dev/null +++ b/src/SubstituteNodeBase.ts @@ -0,0 +1,67 @@ +import { SubstituteBase } from './SubstituteBase' +import { Substitute } from './Substitute' +import { RecordsSet } from './RecordsSet' + +export abstract class SubstituteNodeBase> extends SubstituteBase { + private _parent?: T + private _child?: T + private _head: T & { parent: undefined } + private _root: Substitute + + constructor(private _key: PropertyKey, caller: SubstituteBase) { + super() + + if (caller instanceof Substitute) { + caller.recorder.addIndexedRecord(this) + this._root = caller + } + if (!(caller instanceof SubstituteNodeBase)) return + + this._parent = caller as T + this._head = caller.head as T & { parent: undefined } + caller.child = this + } + + get key(): PropertyKey { + return this._key + } + + set parent(parent: T | undefined) { + this._parent = parent + } + + get parent(): T | undefined { + return this._parent + } + + set child(child: T) { + this._child = child + } + + get child(): T { + return this._child + } + + get head(): T & { parent: undefined } { + return this.isHead() ? this : this._head + } + + protected get root(): Substitute { + return this.head._root + } + + protected isHead(): this is T & { parent: undefined } { + return typeof this._parent === 'undefined' + } + + protected isIntermediateNode(): this is T & { parent: T } { + return !this.isHead() + } + + protected getAllSiblings(): RecordsSet { + return this.root.recorder.getSiblingsOf(this) as RecordsSet + } + + public abstract read(): void + public abstract write(value: any): void +} \ No newline at end of file diff --git a/src/SubstituteProxy.ts b/src/SubstituteProxy.ts new file mode 100644 index 0000000..c8e927c --- /dev/null +++ b/src/SubstituteProxy.ts @@ -0,0 +1,32 @@ +import { SubstituteBase } from './SubstituteBase' +import { SubstituteNode } from './SubstituteNode' + +type BeforeNodeExecutionHook = { + [Handler in keyof ProxyHandler]?: (...args: [..._: Parameters[Handler]>, node: SubstituteNode | undefined, proxy: ProxyHandler]) => void +} +export const createSubstituteProxy = ( + target: T, + beforeNodeExecutionHook: BeforeNodeExecutionHook +) => new Proxy( + target, + { + get: function (this: SubstituteBase, ...args) { + const [target, property] = args + if (target.isSpecialProperty(property)) return target.evaluateSpecialProperty(property) + const newNode = new SubstituteNode(property, target) + beforeNodeExecutionHook.get?.(...args, newNode, this) + return newNode.read() + }, + set: function (...args) { + const [target, property, value] = args + const newNode = new SubstituteNode(property, target) + newNode.write(value) + beforeNodeExecutionHook.set?.(...args, newNode, this) + return true + }, + apply: function (...args) { + return beforeNodeExecutionHook.apply?.(...args, undefined, this) + + } + } +) \ No newline at end of file diff --git a/src/Transformations.ts b/src/Transformations.ts index e6a34b0..f2089c6 100644 --- a/src/Transformations.ts +++ b/src/Transformations.ts @@ -1,4 +1,4 @@ -import { AllArguments } from "./Arguments"; +import { AllArguments } from './Arguments'; type FunctionSubstituteWithOverloads = TFunc extends { @@ -70,6 +70,7 @@ export type ObjectSubstitute = ObjectSub received(amount?: number): TerminatingObject; didNotReceive(): TerminatingObject; mimick(instance: T): void; + clearSubstitute(clearType?: ClearType): void; } type TerminatingFunction = ((...args: TArguments) => void) & ((arg: AllArguments) => void) @@ -92,5 +93,6 @@ type ObjectSubstituteTransformation = { type Omit = Pick>; -export type OmitProxyMethods = Omit; +export type ClearType = 'all' | 'receivedCalls' | 'substituteValues'; +export type OmitProxyMethods = Omit; export type DisabledSubstituteObject = T extends ObjectSubstitute, infer K> ? K : never; diff --git a/src/Utilities.ts b/src/Utilities.ts index 207c05d..53ba35b 100644 --- a/src/Utilities.ts +++ b/src/Utilities.ts @@ -1,99 +1,44 @@ -import { Argument, AllArguments } from './Arguments'; -import { GetPropertyState } from './states/GetPropertyState'; -import { InitialState } from './states/InitialState'; -import { Context } from './Context'; -import * as util from 'util'; - -export type Call = any[] // list of args - -export enum PropertyType { - method = 'method', - property = 'property' -} - -export enum SubstituteMethods { - received = 'received', - didNotReceive = 'didNotReceive', - mimicks = 'mimicks', - throws = 'throws', - returns = 'returns', - resolves = 'resolves', - rejects = 'rejects' -} - -const seenObject = Symbol(); - -export function stringifyArguments(args: any[]) { - args = args.map(x => util.inspect(x)); - return args && args.length > 0 ? 'arguments [' + args.join(', ') + ']' : 'no arguments'; -}; - -export function areArgumentArraysEqual(a: any[], b: any[]) { - if (a.find(x => x instanceof AllArguments) || b.find(b => b instanceof AllArguments)) { - return true; - } - - for (let i = 0; i < Math.max(b.length, a.length); i++) { - if (!areArgumentsEqual(b[i], a[i])) { - return false; - } - } - - return true; -} - -export function stringifyCalls(calls: Call[]) { - - if (calls.length === 0) - return ' (no calls)'; - - let output = ''; - for (let call of calls) { - output += '\n-> call with ' + (call.length ? stringifyArguments(call) : '(no arguments)') - } - - return output; -}; - -export function areArgumentsEqual(a: any, b: any) { - - if (a instanceof Argument && b instanceof Argument) - return false; - - if (a instanceof AllArguments || b instanceof AllArguments) - return true; - - if (a instanceof Argument) - return a.matches(b); - - if (b instanceof Argument) - return b.matches(a); - - return deepEqual(a, b); -}; - -function deepEqual(realA: any, realB: any, objectReferences: object[] = []): boolean { - const a = objectReferences.includes(realA) ? seenObject : realA; - const b = objectReferences.includes(realB) ? seenObject : realB; - const newObjectReferences = updateObjectReferences(objectReferences, a, b); - - if (nonNullObject(a) && nonNullObject(b)) { - if (a.constructor !== b.constructor) return false; - const objectAKeys = Object.keys(a); - if (objectAKeys.length !== Object.keys(b).length) return false; - for (const key of objectAKeys) { - if (!deepEqual(a[key], b[key], newObjectReferences)) return false; - } - return true; - } - return a === b; -} - -function updateObjectReferences(objectReferences: Array, a: any, b: any) { - const tempObjectReferences = [...objectReferences, nonNullObject(a) && !objectReferences.includes(a) ? a : void 0]; - return [...tempObjectReferences, nonNullObject(b) && !tempObjectReferences.includes(b) ? b : void 0]; -} - -function nonNullObject(value: any): value is { [key: string]: any } { - return typeof value === 'object' && value !== null; -} \ No newline at end of file +import { inspect } from 'util' +import { RecordedArguments } from './RecordedArguments' + +export enum PropertyType { + method = 'method', + property = 'property' +} + +export type AssertionMethod = 'received' | 'didNotReceive' +export const isAssertionMethod = (property: PropertyKey): property is AssertionMethod => + property === 'received' || property === 'didNotReceive' + +export type ConfigurationMethod = 'clearSubstitute' +export const isConfigurationMethod = (property: PropertyKey): property is ConfigurationMethod => property === 'clearSubstitute' + +export type SubstitutionMethod = 'mimicks' | 'throws' | 'returns' | 'resolves' | 'rejects' +export const isSubstitutionMethod = (property: PropertyKey): property is SubstitutionMethod => + property === 'mimicks' || property === 'returns' || property === 'throws' || property === 'resolves' || property === 'rejects' + +export const isSubstituteMethod = (property: PropertyKey): property is SubstitutionMethod | ConfigurationMethod | AssertionMethod => + isSubstitutionMethod(property) || isConfigurationMethod(property) || isAssertionMethod(property) + +export const stringifyArguments = (args: RecordedArguments) => textModifier.faint( + args.hasNoArguments + ? 'no arguments' + : `arguments [${args.value.map(x => inspect(x, { colors: true })).join(', ')}]` +) + +export const stringifyCalls = (calls: RecordedArguments[]) => { + if (calls.length === 0) return ' (no calls)' + + const key = '\n-> call with ' + const callsDetails = calls.map(stringifyArguments) + return `${key}${callsDetails.join(key)}` +} + +const baseTextModifier = (str: string, modifierStart: number, modifierEnd: number) => `\x1b[${modifierStart}m${str}\x1b[${modifierEnd}m` +export const textModifier = { + bold: (str: string) => baseTextModifier(str, 1, 22), + faint: (str: string) => baseTextModifier(str, 2, 22), + italic: (str: string) => baseTextModifier(str, 3, 23) +} + +export const plurify = (str: string, count: number) => `${str}${count === 1 ? '' : 's'}` \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 2177c84..c054fa3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ -import { Substitute, SubstituteOf } from './Substitute'; +import { Substitute, SubstituteOf } from './Substitute' -export { Arg } from './Arguments'; -export { Substitute, SubstituteOf }; +export { Arg } from './Arguments' +export { Substitute, SubstituteOf } -export default Substitute; \ No newline at end of file +export default Substitute \ No newline at end of file diff --git a/src/states/ContextState.ts b/src/states/ContextState.ts deleted file mode 100644 index 6448fbd..0000000 --- a/src/states/ContextState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Context } from "../Context"; - -export type PropertyKey = string | number | symbol; - -export interface ContextState { - onSwitchedTo?(context: Context): void; - apply(context: Context, args: any[]): any; - set(context: Context, property: PropertyKey, value: any): void; - get(context: Context, property: PropertyKey): any; -} \ No newline at end of file diff --git a/src/states/GetPropertyState.ts b/src/states/GetPropertyState.ts deleted file mode 100644 index a5abd91..0000000 --- a/src/states/GetPropertyState.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { ContextState, PropertyKey } from './ContextState'; -import { Context } from '../Context'; -import { PropertyType, SubstituteMethods, Call, areArgumentArraysEqual } from '../Utilities'; -import { SubstituteException } from '../SubstituteBase'; - -interface SubstituteMock { - arguments: Call - mockValues: any[] - substituteType: SubstituteMethods -} - -export class GetPropertyState implements ContextState { - private _mocks: SubstituteMock[]; - private _recordedCalls: Call[]; - private _isFunctionState: boolean; - private _lastArgs?: Call; - - public get property(): PropertyKey { - return this._property; - } - - get isFunctionState(): boolean { - return this._isFunctionState; - } - - public get callCount(): number { - return this._recordedCalls.length; - } - - constructor(private _property: PropertyKey) { - this._mocks = []; - this._recordedCalls = []; - this._isFunctionState = false; - } - - private getCallCount(args: Call): number { - const callFilter = (recordedCall: Call): boolean => areArgumentArraysEqual(recordedCall, args); - return this._recordedCalls.filter(callFilter).length; - } - - private applySubstituteMethodLogic(substituteMethod: SubstituteMethods, mockValue: any, args?: Call) { - switch (substituteMethod) { - case SubstituteMethods.resolves: - return Promise.resolve(mockValue); - case SubstituteMethods.rejects: - return Promise.reject(mockValue); - case SubstituteMethods.returns: - return mockValue; - case SubstituteMethods.throws: - throw mockValue; - case SubstituteMethods.mimicks: - return mockValue.apply(mockValue, args); - default: - throw SubstituteException.generic(`Method ${substituteMethod} not implemented`) - } - } - - private processProperty(context: Context, args: any[], propertyType: PropertyType) { - const hasExpectations = context.initialState.hasExpectations; - if (!hasExpectations) { - this._recordedCalls.push(args); - const foundSubstitute = this._mocks.find(mock => areArgumentArraysEqual(mock.arguments, args)); - if (foundSubstitute !== void 0) { - const mockValue = foundSubstitute.mockValues.length > 1 ? - foundSubstitute.mockValues.shift() : - foundSubstitute.mockValues[0]; - return this.applySubstituteMethodLogic(foundSubstitute.substituteType, mockValue, args); - } - } - - context.initialState.assertCallCountMatchesExpectations( - this._recordedCalls, - this.getCallCount(args), - propertyType, - this.property, - args - ); - - return context.proxy; - } - - apply(context: Context, args: any[]) { - if (!this._isFunctionState) { - this._isFunctionState = true; - this._recordedCalls = []; - } - this._lastArgs = args; - return this.processProperty(context, args, PropertyType.method); - } - - set(context: Context, property: PropertyKey, value: any) { } - - private isSubstituteMethod(property: PropertyKey): property is SubstituteMethods { - return property === SubstituteMethods.returns || - property === SubstituteMethods.mimicks || - property === SubstituteMethods.throws || - property === SubstituteMethods.resolves || - property === SubstituteMethods.rejects; - } - - private sanitizeSubstituteMockInputs(mockInputs: Call): Call { - if (mockInputs.length === 0) return [undefined]; - return mockInputs.length > 1 ? - [...mockInputs, undefined] : - [...mockInputs]; - } - - get(context: Context, property: PropertyKey) { - if (property === 'then') return void 0; - - if (this.isSubstituteMethod(property)) { - return (...inputs: Call) => { - const mockInputs = this.sanitizeSubstituteMockInputs(inputs); - const args = this._isFunctionState ? this._lastArgs : []; - if (args === void 0) - throw SubstituteException.generic('Eh, there\'s a bug, no args recorded :/'); - - this._mocks.push({ - arguments: args, - mockValues: mockInputs, - substituteType: property - }); - - this._recordedCalls.pop(); - context.state = context.initialState; - } - } - if (this._isFunctionState) return context.proxy; - return this.processProperty(context, [], PropertyType.property); - } -} \ No newline at end of file diff --git a/src/states/InitialState.ts b/src/states/InitialState.ts deleted file mode 100644 index d7648ec..0000000 --- a/src/states/InitialState.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { ContextState, PropertyKey } from './ContextState'; -import { Context } from '../Context'; -import { GetPropertyState } from './GetPropertyState'; -import { SetPropertyState } from './SetPropertyState'; -import { SubstituteMethods, Call, PropertyType } from '../Utilities'; -import { AreProxiesDisabledKey } from '../Substitute'; -import { SubstituteException } from '../SubstituteBase'; - -export class InitialState implements ContextState { - private recordedGetPropertyStates: Map; - private recordedSetPropertyStates: SetPropertyState[]; - - private _expectedCount: number | undefined | null; - private _areProxiesDisabled: boolean; - - public get expectedCount(): number | undefined | null { - return this._expectedCount; - } - - public get hasExpectations(): boolean { - return this._expectedCount !== void 0; - } - - public get setPropertyStates(): SetPropertyState[] { - return [...this.recordedSetPropertyStates]; - } - - public get getPropertyStates(): GetPropertyState[] { - return [...this.recordedGetPropertyStates.values()]; - } - - public recordGetPropertyState(property: PropertyKey, getState: GetPropertyState): void { - this.recordedGetPropertyStates.set(property, getState); - } - - public recordSetPropertyState(setState: SetPropertyState): void { - this.recordedSetPropertyStates.push(setState); - } - - constructor() { - this.recordedGetPropertyStates = new Map(); - this.recordedSetPropertyStates = []; - - this._areProxiesDisabled = false; - this._expectedCount = void 0; - } - - public assertCallCountMatchesExpectations( - receivedCalls: Call[], - receivedCount: number, - type: PropertyType, - propertyValue: PropertyKey, - args: any[] - ): void | never { - const expectedCount = this._expectedCount; - - this.clearExpectations(); - if (this.doesCallCountMatchExpectations(expectedCount, receivedCount)) - return; - - const callCount = { expected: expectedCount, received: receivedCount }; - const property = { type, value: propertyValue }; - const calls = { expectedArguments: args, received: receivedCalls }; - - throw SubstituteException.forCallCountMissMatch(callCount, property, calls); - } - - private doesCallCountMatchExpectations(expectedCount: number | undefined | null, actualCount: number) { - if (expectedCount === void 0) - return true; - - if (expectedCount === null && actualCount > 0) - return true; - - return expectedCount === actualCount; - } - - apply(context: Context, args: any[]) { } - - set(context: Context, property: PropertyKey, value: any) { - if (property === AreProxiesDisabledKey) { - this._areProxiesDisabled = value; - return; - } - - const existingSetState = this.recordedSetPropertyStates.find(x => x.arguments[0] === value); - if (existingSetState) { - return existingSetState.set(context, property, value); - } - - const setPropertyState = new SetPropertyState(property, value); - this.recordedSetPropertyStates.push(setPropertyState); - - context.state = setPropertyState; - return context.setStateSet(context, property, value); - } - - get(context: Context, property: PropertyKey) { - switch (property) { - case AreProxiesDisabledKey: - return this._areProxiesDisabled; - case SubstituteMethods.received: - return (count?: number) => { - this._expectedCount = count ?? null; - return context.receivedProxy; - }; - case SubstituteMethods.didNotReceive: - return () => { - this._expectedCount = 0; - return context.receivedProxy; - }; - default: - return this.handleGet(context, property); - } - } - - private clearExpectations() { - this._expectedCount = void 0; - } - - onSwitchedTo() { - this.clearExpectations(); - } - - public handleGet(context: Context, property: PropertyKey) { - const existingGetState = this.getPropertyStates.find(state => state.property === property); - if (existingGetState !== void 0) { - context.state = existingGetState; - return context.getStateGet(void 0, property); - } - - const getState = new GetPropertyState(property); - this.recordGetPropertyState(property, getState); - - context.state = getState; - return context.getStateGet(void 0, property); - } -} \ No newline at end of file diff --git a/src/states/SetPropertyState.ts b/src/states/SetPropertyState.ts deleted file mode 100644 index c7e62fd..0000000 --- a/src/states/SetPropertyState.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ContextState, PropertyKey } from './ContextState'; -import { Context } from '../Context'; -import { areArgumentsEqual, PropertyType } from '../Utilities'; -import { SubstituteException } from '../SubstituteBase'; - -export class SetPropertyState implements ContextState { - private _callCount: number; - private _arguments: any[]; - - public get arguments() { - return this._arguments; - } - - public get property() { - return this._property; - } - - public get callCount() { - return this._callCount; - } - - constructor(private _property: PropertyKey, ...args: any[]) { - this._arguments = args; - this._callCount = 0; - } - - apply(context: Context): undefined { - throw SubstituteException.generic('Calling apply of setPropertyState is not normal behaviour, something went wrong'); - } - - set(context: Context, property: PropertyKey, value: any) { - let callCount = this._callCount; - const hasExpectations = context.initialState.hasExpectations; - if (hasExpectations) { - callCount = context.initialState - .setPropertyStates - .filter(x => areArgumentsEqual(x.arguments[0], value)) - .map(x => x._callCount) - .reduce((a, b) => a + b, 0); - } - - context.initialState.assertCallCountMatchesExpectations( - [[]], - callCount, - PropertyType.property, - this.property, - this.arguments - ); - - if (!hasExpectations) { - this._callCount++; - } - } - - get(context: Context, property: PropertyKey): undefined { - throw SubstituteException.generic('Calling get of setPropertyState is not normal behaviour, something went wrong'); - } -} \ No newline at end of file