From 8dfd143208697bfb87874e80624c0262131fc01a Mon Sep 17 00:00:00 2001 From: Justin Domingue Date: Mon, 26 Jul 2021 10:41:08 -0700 Subject: [PATCH] test: set up component snapshot testing (#2102) * set up snapshot testing * improvements * add tests for TextInput as an example * Fix code style issues with ESLint * add comment to custom-test-env file * only set up needed providers * include style rules in snapshots * disable redux storage warning * added setupTests to avoid boilerplate Co-authored-by: Lint Action --- custom-test-env.js | 16 ++ package.json | 5 +- .../__snapshots__/index.test.tsx.snap | 130 ++++++++++++++++ src/components/TextInput/index.test.tsx | 68 +++++++++ src/setupTests.ts | 5 + src/state/index.ts | 2 +- src/test-utils.tsx | 19 +++ yarn.lock | 143 +++++++++++++++++- 8 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 custom-test-env.js create mode 100644 src/components/TextInput/__snapshots__/index.test.tsx.snap create mode 100644 src/components/TextInput/index.test.tsx create mode 100644 src/setupTests.ts create mode 100644 src/test-utils.tsx diff --git a/custom-test-env.js b/custom-test-env.js new file mode 100644 index 000000000..3cbb28b21 --- /dev/null +++ b/custom-test-env.js @@ -0,0 +1,16 @@ +// Custom test environment to provide `TextEncoder`/`TextDecoder` + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const Environment = require('jest-environment-jsdom') + +module.exports = class CustomTestEnvironment extends Environment { + async setup() { + await super.setup() + if (typeof this.global.TextEncoder === 'undefined') { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { TextEncoder, TextDecoder } = require('util') + this.global.TextEncoder = TextEncoder + this.global.TextDecoder = TextDecoder + } + } +} diff --git a/package.json b/package.json index 9b46fb432..4d0cc6d1e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "@reach/portal": "^0.10.3", "@react-hook/window-scroll": "^1.3.0", "@reduxjs/toolkit": "^1.6.1", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^12.0.0", "@typechain/ethers-v5": "^7.0.0", "@types/d3": "^6.7.1", "@types/jest": "^25.2.1", @@ -77,6 +79,7 @@ "graphql": "^15.5.0", "graphql-request": "^3.4.0", "inter-ui": "^3.13.1", + "jest-styled-components": "^7.0.5", "lodash.flatmap": "^4.5.0", "luxon": "^1.25.0", "ms.macro": "^2.0.0", @@ -134,7 +137,7 @@ "graphql:generate": "graphql-codegen --config codegen.yml", "postinstall": "yarn compile-contract-types", "start": "yarn compile-contract-types && react-scripts start", - "test": "react-scripts test --env=jsdom", + "test": "react-scripts test --env=./custom-test-env.js", "prestart": "yarn graphql:generate && touch src/locales/en-US.po" }, "eslintConfig": { diff --git a/src/components/TextInput/__snapshots__/index.test.tsx.snap b/src/components/TextInput/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..0002eebb5 --- /dev/null +++ b/src/components/TextInput/__snapshots__/index.test.tsx.snap @@ -0,0 +1,130 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResizableTextArea renders correctly 1`] = ` + + .c0 { + font-size: 12; + outline: none; + border: none; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 0; + resize: none; + background-color: #F7F8FA; + -webkit-transition: color 300ms step-start; + transition: color 300ms step-start; + color: #000000; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 500; + width: 100%; + line-height: 1.2; + padding: 0px; + -webkit-appearance: textfield; +} + +.c0::-webkit-search-decoration { + -webkit-appearance: none; +} + +.c0::-webkit-outer-spin-button, +.c0::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +.c0::-webkit-input-placeholder { + color: #C3C5CB; +} + +.c0::-moz-placeholder { + color: #C3C5CB; +} + +.c0:-ms-input-placeholder { + color: #C3C5CB; +} + +.c0::placeholder { + color: #C3C5CB; +} + + + +`; + +exports[`TextInput renders correctly 1`] = ` + + .c0 { + font-size: 12; + outline: none; + border: none; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 0; + background-color: #F7F8FA; + -webkit-transition: color 300ms step-start; + transition: color 300ms step-start; + color: #000000; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 500; + width: 100%; + padding: 0px; + -webkit-appearance: textfield; +} + +.c0::-webkit-search-decoration { + -webkit-appearance: none; +} + +.c0::-webkit-outer-spin-button, +.c0::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +.c0::-webkit-input-placeholder { + color: #C3C5CB; +} + +.c0::-moz-placeholder { + color: #C3C5CB; +} + +.c0:-ms-input-placeholder { + color: #C3C5CB; +} + +.c0::placeholder { + color: #C3C5CB; +} + +
+ +
+
+`; diff --git a/src/components/TextInput/index.test.tsx b/src/components/TextInput/index.test.tsx new file mode 100644 index 000000000..107a385b0 --- /dev/null +++ b/src/components/TextInput/index.test.tsx @@ -0,0 +1,68 @@ +import { TextInput, ResizingTextArea } from './' +import { render, screen, fireEvent } from 'test-utils' + +describe('TextInput', () => { + it('renders correctly', () => { + const { asFragment } = render( + null} + placeholder="Test Placeholder" + fontSize="12" + /> + ) + expect(asFragment()).toMatchSnapshot() + }) + + it('calls the handler on user input', () => { + const onUserInputSpy = jest.fn() + render( + + ) + + fireEvent.change(screen.getByPlaceholderText('Test Placeholder'), { target: { value: 'New value' } }) + + expect(onUserInputSpy).toHaveBeenCalledWith('New value') + expect(onUserInputSpy).toHaveBeenCalledTimes(1) + }) +}) + +describe('ResizableTextArea', () => { + it('renders correctly', () => { + const { asFragment } = render( + null} + placeholder="Test Placeholder" + fontSize="12" + /> + ) + expect(asFragment()).toMatchSnapshot() + }) + + it('calls the handler on user input', () => { + const onUserInputSpy = jest.fn() + render( + + ) + + fireEvent.change(screen.getByPlaceholderText('Test Placeholder'), { target: { value: 'New value' } }) + + expect(onUserInputSpy).toHaveBeenCalledWith('New value') + expect(onUserInputSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 000000000..c8d5b4164 --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1,5 @@ +// jest custom assertions +import '@testing-library/jest-dom' + +// include style rules in snapshots +import 'jest-styled-components' diff --git a/src/state/index.ts b/src/state/index.ts index 9ac0d9996..162f8d0d4 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -39,7 +39,7 @@ const store = configureStore({ .concat(dataApi.middleware) .concat(routingApi.middleware) .concat(save({ states: PERSISTED_KEYS, debounce: 1000 })), - preloadedState: load({ states: PERSISTED_KEYS }), + preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: process.env.NODE_ENV === 'test' }), }) store.dispatch(updateVersion()) diff --git a/src/test-utils.tsx b/src/test-utils.tsx new file mode 100644 index 000000000..0721b92a3 --- /dev/null +++ b/src/test-utils.tsx @@ -0,0 +1,19 @@ +import React, { FC, ReactElement, ReactNode } from 'react' +import { render, RenderOptions } from '@testing-library/react' +import ThemeProvider from 'theme' +import store from 'state' +import { Provider } from 'react-redux' + +const WithProviders: FC = ({ children }: { children?: ReactNode }) => { + return ( + + {children} + + ) +} + +const customRender = (ui: ReactElement, options?: Omit) => + render(ui, { wrapper: WithProviders, ...options }) + +export * from '@testing-library/react' +export { customRender as render } diff --git a/yarn.lock b/yarn.lock index 2bed394e0..fd9b21527 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2461,6 +2461,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^27.0.6": + version "27.0.6" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.0.6.tgz#9a992bc517e0c49f035938b8549719c2de40706b" + integrity sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + "@jimp/bmp@^0.16.1": version "0.16.1" resolved "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.16.1.tgz" @@ -3181,6 +3192,43 @@ lz-string "^1.4.4" pretty-format "^26.6.2" +"@testing-library/dom@^8.0.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.1.0.tgz#f8358b1883844ea569ba76b7e94582168df5370d" + integrity sha512-kmW9alndr19qd6DABzQ978zKQ+J65gU2Rzkl8hriIetPnwpesRaK4//jEQyYh8fEALmGhomD/LBQqt+o+DL95Q== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^4.2.2" + chalk "^4.1.0" + dom-accessibility-api "^0.5.6" + lz-string "^1.4.4" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@^5.14.1": + version "5.14.1" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz#8501e16f1e55a55d675fe73eecee32cdaddb9766" + integrity sha512-dfB7HVIgTNCxH22M1+KU6viG5of2ldoA5ly8Ar8xkezKHKXjRvznCdbMbqjYGgO2xjRbwnR+rR8MLUIqF3kKbQ== + dependencies: + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^4.2.2" + chalk "^3.0.0" + css "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.5.6" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.0.0.tgz#9aeb2264521522ab9b68f519eaf15136148f164a" + integrity sha512-sh3jhFgEshFyJ/0IxGltRhwZv2kFKfJ3fN1vTZ6hhMXzz9ZbbcTgmDYM4e+zJv+oiVKKEWZPyqPAh4MQBI65gA== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.0.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -3536,6 +3584,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@*": + version "26.0.24" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a" + integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + "@types/jest@^25.2.1": version "25.2.3" resolved "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz" @@ -3825,6 +3881,13 @@ "@testing-library/dom" "^7.11.0" cypress "*" +"@types/testing-library__jest-dom@^5.9.1": + version "5.14.1" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz#014162a5cee6571819d48e999980694e2f657c3c" + integrity sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw== + dependencies: + "@types/jest" "*" + "@types/ua-parser-js@^0.7.35": version "0.7.36" resolved "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz" @@ -3887,6 +3950,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + "@types/yauzl@^2.9.1": version "2.9.2" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" @@ -4747,6 +4817,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + any-base@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz" @@ -7475,6 +7550,11 @@ css-what@^5.0.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + css@^2.0.0: version "2.2.4" resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" @@ -7485,6 +7565,15 @@ css@^2.0.0: source-map-resolve "^0.5.2" urix "^0.1.0" +css@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" + integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== + dependencies: + inherits "^2.0.4" + source-map "^0.6.1" + source-map-resolve "^0.6.0" + cssdb@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" @@ -8315,6 +8404,11 @@ dom-accessibility-api@^0.5.4: resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz" integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== +dom-accessibility-api@^0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz#3f5d43b52c7a3bd68b5fb63fa47b4e4c1fdf65a9" + integrity sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw== + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -11762,7 +11856,7 @@ jest-diff@^25.2.1: jest-get-type "^25.2.6" pretty-format "^25.5.0" -jest-diff@^26.6.2: +jest-diff@^26.0.0, jest-diff@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== @@ -12047,6 +12141,13 @@ jest-snapshot@^26.6.0, jest-snapshot@^26.6.2: pretty-format "^26.6.2" semver "^7.3.2" +jest-styled-components@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-7.0.5.tgz#6da3f1a1c8bd98bccc6bcf9aabfdefdcd88fd92c" + integrity sha512-ZR/r3IKNkgaaVIOThn0Qis4sNQtA352qHjhbxSHeLS3FDIvHSUSJoI2b3kzk+bHHQ1VOeV630usERtnyhyZh4A== + dependencies: + css "^3.0.0" + jest-util@^26.6.0, jest-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" @@ -13226,6 +13327,11 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + mini-create-react-context@^0.4.0: version "0.4.1" resolved "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz" @@ -15163,7 +15269,7 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-format@^26.6.0, pretty-format@^26.6.2: +pretty-format@^26.0.0, pretty-format@^26.6.0, pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== @@ -15173,6 +15279,16 @@ pretty-format@^26.6.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-format@^27.0.2: + version "27.0.6" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.0.6.tgz#ab770c47b2c6f893a21aefc57b75da63ef49a11f" + integrity sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ== + dependencies: + "@jest/types" "^27.0.6" + ansi-regex "^5.0.0" + ansi-styles "^5.0.0" + react-is "^17.0.1" + private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.npmjs.org/private/-/private-0.1.8.tgz" @@ -15895,6 +16011,14 @@ recursive-readdir@2.2.2: dependencies: minimatch "3.0.4" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + redux-devtools-extension@^2.13.9: version "2.13.9" resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz#6b764e8028b507adcb75a1cae790f71e6be08ae7" @@ -17045,6 +17169,14 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + source-map-support@^0.4.15: version "0.4.18" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz" @@ -17474,6 +17606,13 @@ strip-hex-prefix@1.0.0: dependencies: is-hex-prefixed "1.0.0" +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"