Mirror: The spec-compliant minimum of client-side GraphQL.

feat: Add tests and enforce test coverage (#8)

* Set up coverage provider

* Add test for multiline block string

* Fix up Int/Float detection

* Add test for non-block strings

* Add tests for object values

* Reformat parse tests

* Add tests for parsing arguments

* Add tests for directives

* Add test for aliased field

* Add tests for list values

* Add tests for invalid type references

* Add tests for inline fragments

* Add tests for variable definitions

* Remove redundant type parse check

* Add tests for fragment definitions

* Remove redundant EOF check

* Remove redundant null check in parseType

* Add basic call tests

* Update GraphQLError and add tests

* Apply lints

* Add tests for printString and printBlockString

* Add additional printer tests

* Add missing test cases for visitor

* Add changeset

* Update snapshots

* Apply lints

+5
.changeset/cold-llamas-grin.md
···
+
---
+
'@0no-co/graphql.web': patch
+
---
+
+
Fix float pattern and int/float decision in value parsing.
+5
.changeset/fifty-carrots-serve.md
···
+
---
+
'@0no-co/graphql.web': patch
+
---
+
+
Remove redundant code paths from `visit` and parser.
+1 -1
.github/workflows/ci.yml
···
run: pnpm run lint
- name: Unit Tests
-
run: pnpm run test
+
run: pnpm run test --run
- name: Build
run: pnpm run build
+1
.gitignore
···
.rts2_cache*
.husky
dist/
+
coverage/
package-lock.json
.DS_Store
+2 -7
benchmark/suite.js
···
const graphql16 = require('graphql16');
const graphql17 = require('graphql17');
-
const kitchenSink =
-
fs.readFileSync('../src/__tests__/kitchen_sink.graphql', { encoding: 'utf8' });
+
const kitchenSink = fs.readFileSync('../src/__tests__/kitchen_sink.graphql', { encoding: 'utf8' });
const document = require('../src/__tests__/kitchen_sink.json');
suite('parse kitchen sink query', () => {
···
function formatNode(node) {
if (!node.selectionSet) return node;
for (const selection of node.selectionSet.selections)
-
if (
-
selection.kind === 'Field' &&
-
selection.name.value === '__typename' &&
-
!selection.alias
-
)
+
if (selection.kind === 'Field' && selection.name.value === '__typename' && !selection.alias)
return node;
return {
+3 -2
package.json
···
"client-side graphql"
],
"scripts": {
-
"test": "vitest run",
+
"test": "vitest",
"check": "tsc",
"lint": "eslint --ext=js,ts .",
"build": "rollup -c scripts/rollup.config.mjs",
···
"@rollup/plugin-terser": "^0.4.0",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
+
"@vitest/coverage-c8": "^0.29.7",
"dotenv": "^16.0.3",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.7.0",
···
"rollup-plugin-dts": "^5.3.0",
"terser": "^5.16.6",
"typescript": "^5.0.2",
-
"vitest": "^0.29.3"
+
"vitest": "^0.29.7"
},
"publishConfig": {
"access": "public"
+170 -25
pnpm-lock.yaml
···
'@rollup/plugin-terser': ^0.4.0
'@typescript-eslint/eslint-plugin': ^5.55.0
'@typescript-eslint/parser': ^5.55.0
+
'@vitest/coverage-c8': ^0.29.7
dotenv: ^16.0.3
eslint: ^8.36.0
eslint-config-prettier: ^8.7.0
···
rollup-plugin-dts: ^5.3.0
terser: ^5.16.6
typescript: ^5.0.2
-
vitest: ^0.29.3
+
vitest: ^0.29.7
devDependencies:
'@changesets/cli': 2.26.0
'@changesets/get-github-info': 0.5.2
···
'@rollup/plugin-terser': 0.4.0_rollup@3.19.1
'@typescript-eslint/eslint-plugin': 5.55.0_qsnvknysi52qtaxqdyqyohkcku
'@typescript-eslint/parser': 5.55.0_j4766f7ecgqbon3u7zlxn5zszu
+
'@vitest/coverage-c8': 0.29.7_vitest@0.29.7
dotenv: 16.0.3
eslint: 8.36.0
eslint-config-prettier: 8.7.0_eslint@8.36.0
···
rollup-plugin-dts: 5.3.0_7iejawhbqmte5pthjozf4tfuqy
terser: 5.16.6
typescript: 5.0.2
-
vitest: 0.29.3_terser@5.16.6
+
vitest: 0.29.7_terser@5.16.6
benchmark:
specifiers:
···
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.11
+
dev: true
+
+
/@bcoe/v8-coverage/0.2.3:
+
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true
/@changesets/apply-release-plan/6.1.3:
···
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true
+
/@istanbuljs/schema/0.1.3:
+
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
+
engines: {node: '>=8'}
+
dev: true
+
/@jridgewell/gen-mapping/0.3.2:
resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
engines: {node: '>=6.0.0'}
···
ci-info: 3.8.0
dev: true
+
/@types/istanbul-lib-coverage/2.0.4:
+
resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
+
dev: true
+
/@types/json-schema/7.0.11:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
···
eslint-visitor-keys: 3.3.0
dev: true
-
/@vitest/expect/0.29.3:
-
resolution: {integrity: sha512-z/0JqBqqrdtrT/wzxNrWC76EpkOHdl+SvuNGxWulLaoluygntYyG5wJul5u/rQs5875zfFz/F+JaDf90SkLUIg==}
+
/@vitest/coverage-c8/0.29.7_vitest@0.29.7:
+
resolution: {integrity: sha512-TSubtP9JFBuI/wuApxwknHe40VDkX8hFbBak0OXj4/jCeXrEu5B5GPWcxzyk9YvzXgCaDvoiZV79I7AvhNI9YQ==}
+
peerDependencies:
+
vitest: '>=0.29.0 <1'
+
dependencies:
+
c8: 7.13.0
+
picocolors: 1.0.0
+
std-env: 3.3.2
+
vitest: 0.29.7_terser@5.16.6
+
dev: true
+
+
/@vitest/expect/0.29.7:
+
resolution: {integrity: sha512-UtG0tW0DP6b3N8aw7PHmweKDsvPv4wjGvrVZW7OSxaFg76ShtVdMiMcUkZJgCE8QWUmhwaM0aQhbbVLo4F4pkA==}
dependencies:
-
'@vitest/spy': 0.29.3
-
'@vitest/utils': 0.29.3
+
'@vitest/spy': 0.29.7
+
'@vitest/utils': 0.29.7
chai: 4.3.7
dev: true
-
/@vitest/runner/0.29.3:
-
resolution: {integrity: sha512-XLi8ctbvOWhUWmuvBUSIBf8POEDH4zCh6bOuVxm/KGfARpgmVF1ku+vVNvyq85va+7qXxtl+MFmzyXQ2xzhAvw==}
+
/@vitest/runner/0.29.7:
+
resolution: {integrity: sha512-Yt0+csM945+odOx4rjZSjibQfl2ymxqVsmYz6sO2fiO5RGPYDFCo60JF6tLL9pz4G/kjY4irUxadeB1XT+H1jg==}
dependencies:
-
'@vitest/utils': 0.29.3
+
'@vitest/utils': 0.29.7
p-limit: 4.0.0
pathe: 1.1.0
dev: true
-
/@vitest/spy/0.29.3:
-
resolution: {integrity: sha512-LLpCb1oOCOZcBm0/Oxbr1DQTuKLRBsSIHyLYof7z4QVE8/v8NcZKdORjMUq645fcfX55+nLXwU/1AQ+c2rND+w==}
+
/@vitest/spy/0.29.7:
+
resolution: {integrity: sha512-IalL0iO6A6Xz8hthR8sctk6ZS//zVBX48EiNwQguYACdgdei9ZhwMaBFV70mpmeYAFCRAm+DpoFHM5470Im78A==}
dependencies:
tinyspy: 1.1.1
dev: true
-
/@vitest/utils/0.29.3:
-
resolution: {integrity: sha512-hg4Ff8AM1GtUnLpUJlNMxrf9f4lZr/xRJjh3uJ0QFP+vjaW82HAxKrmeBmLnhc8Os2eRf+f+VBu4ts7TafPPkA==}
+
/@vitest/utils/0.29.7:
+
resolution: {integrity: sha512-vNgGadp2eE5XKCXtZXL5UyNEDn68npSct75OC9AlELenSK0DiV1Mb9tfkwJHKjRb69iek+e79iipoJx8+s3SdA==}
dependencies:
cli-truncate: 3.1.0
diff: 5.1.0
···
engines: {node: '>=6'}
dev: true
+
/c8/7.13.0:
+
resolution: {integrity: sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==}
+
engines: {node: '>=10.12.0'}
+
hasBin: true
+
dependencies:
+
'@bcoe/v8-coverage': 0.2.3
+
'@istanbuljs/schema': 0.1.3
+
find-up: 5.0.0
+
foreground-child: 2.0.0
+
istanbul-lib-coverage: 3.2.0
+
istanbul-lib-report: 3.0.0
+
istanbul-reports: 3.1.5
+
rimraf: 3.0.2
+
test-exclude: 6.0.0
+
v8-to-istanbul: 9.1.0
+
yargs: 16.2.0
+
yargs-parser: 20.2.9
+
dev: true
+
/cac/6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
···
wrap-ansi: 6.2.0
dev: true
+
/cliui/7.0.4:
+
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+
dependencies:
+
string-width: 4.2.3
+
strip-ansi: 6.0.1
+
wrap-ansi: 7.0.0
+
dev: true
+
/cliui/8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
···
/concat-map/0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
dev: true
+
+
/convert-source-map/1.9.0:
+
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
dev: true
/cosmiconfig/7.1.0:
···
is-callable: 1.2.7
dev: true
+
/foreground-child/2.0.0:
+
resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==}
+
engines: {node: '>=8.0.0'}
+
dependencies:
+
cross-spawn: 7.0.3
+
signal-exit: 3.0.7
+
dev: true
+
/fs-extra/7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
···
/hosted-git-info/2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
+
dev: true
+
+
/html-escaper/2.0.2:
+
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
dev: true
/human-id/1.0.2:
···
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
+
/istanbul-lib-coverage/3.2.0:
+
resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==}
+
engines: {node: '>=8'}
+
dev: true
+
+
/istanbul-lib-report/3.0.0:
+
resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==}
+
engines: {node: '>=8'}
+
dependencies:
+
istanbul-lib-coverage: 3.2.0
+
make-dir: 3.1.0
+
supports-color: 7.2.0
+
dev: true
+
+
/istanbul-reports/3.1.5:
+
resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==}
+
engines: {node: '>=8'}
+
dependencies:
+
html-escaper: 2.0.2
+
istanbul-lib-report: 3.0.0
+
dev: true
+
/jju/1.4.0:
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
dev: true
···
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
+
/make-dir/3.1.0:
+
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
+
engines: {node: '>=8'}
+
dependencies:
+
semver: 6.3.0
+
dev: true
+
/map-obj/1.0.1:
resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
engines: {node: '>=0.10.0'}
···
hasBin: true
dev: true
+
/semver/6.3.0:
+
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
+
hasBin: true
+
dev: true
+
/semver/7.3.8:
resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
engines: {node: '>=10'}
···
source-map-support: 0.5.21
dev: true
+
/test-exclude/6.0.0:
+
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
+
engines: {node: '>=8'}
+
dependencies:
+
'@istanbuljs/schema': 0.1.3
+
glob: 7.2.3
+
minimatch: 3.1.2
+
dev: true
+
/text-table/0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true
···
resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==}
dev: true
-
/tinypool/0.3.1:
-
resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==}
+
/tinypool/0.4.0:
+
resolution: {integrity: sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==}
engines: {node: '>=14.0.0'}
dev: true
···
punycode: 2.3.0
dev: true
+
/v8-to-istanbul/9.1.0:
+
resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==}
+
engines: {node: '>=10.12.0'}
+
dependencies:
+
'@jridgewell/trace-mapping': 0.3.17
+
'@types/istanbul-lib-coverage': 2.0.4
+
convert-source-map: 1.9.0
+
dev: true
+
/validate-npm-package-license/3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
dependencies:
···
spdx-expression-parse: 3.0.1
dev: true
-
/vite-node/0.29.3_67ayhxtn77ihpqz7ip4pro4g64:
-
resolution: {integrity: sha512-QYzYSA4Yt2IiduEjYbccfZQfxKp+T1Do8/HEpSX/G5WIECTFKJADwLs9c94aQH4o0A+UtCKU61lj1m5KvbxxQA==}
+
/vite-node/0.29.7_67ayhxtn77ihpqz7ip4pro4g64:
+
resolution: {integrity: sha512-PakCZLvz37yFfUPWBnLa1OYHPCGm5v4pmRrTcFN4V/N/T3I6tyP3z07S//9w+DdeL7vVd0VSeyMZuAh+449ZWw==}
engines: {node: '>=v14.16.0'}
hasBin: true
dependencies:
···
fsevents: 2.3.2
dev: true
-
/vitest/0.29.3_terser@5.16.6:
-
resolution: {integrity: sha512-muMsbXnZsrzDGiyqf/09BKQsGeUxxlyLeLK/sFFM4EXdURPQRv8y7dco32DXaRORYP0bvyN19C835dT23mL0ow==}
+
/vitest/0.29.7_terser@5.16.6:
+
resolution: {integrity: sha512-aWinOSOu4jwTuZHkb+cCyrqQ116Q9TXaJrNKTHudKBknIpR0VplzeaOUuDF9jeZcrbtQKZQt6yrtd+eakbaxHg==}
engines: {node: '>=v14.16.0'}
hasBin: true
peerDependencies:
···
'@vitest/ui': '*'
happy-dom: '*'
jsdom: '*'
+
safaridriver: '*'
+
webdriverio: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
···
optional: true
jsdom:
optional: true
+
safaridriver:
+
optional: true
+
webdriverio:
+
optional: true
dependencies:
'@types/chai': 4.3.4
'@types/chai-subset': 1.3.3
'@types/node': 18.15.3
-
'@vitest/expect': 0.29.3
-
'@vitest/runner': 0.29.3
-
'@vitest/spy': 0.29.3
-
'@vitest/utils': 0.29.3
+
'@vitest/expect': 0.29.7
+
'@vitest/runner': 0.29.7
+
'@vitest/spy': 0.29.7
+
'@vitest/utils': 0.29.7
acorn: 8.8.2
acorn-walk: 8.2.0
cac: 6.7.14
···
std-env: 3.3.2
strip-literal: 1.0.1
tinybench: 2.4.0
-
tinypool: 0.3.1
+
tinypool: 0.4.0
tinyspy: 1.1.1
vite: 4.2.0_67ayhxtn77ihpqz7ip4pro4g64
-
vite-node: 0.29.3_67ayhxtn77ihpqz7ip4pro4g64
+
vite-node: 0.29.7_67ayhxtn77ihpqz7ip4pro4g64
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less
···
decamelize: 1.2.0
dev: true
+
/yargs-parser/20.2.9:
+
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
+
engines: {node: '>=10'}
+
dev: true
+
/yargs-parser/21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
···
which-module: 2.0.0
y18n: 4.0.3
yargs-parser: 18.1.3
+
dev: true
+
+
/yargs/16.2.0:
+
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
+
engines: {node: '>=10'}
+
dependencies:
+
cliui: 7.0.4
+
escalade: 3.1.1
+
get-caller-file: 2.0.5
+
require-directory: 2.1.1
+
string-width: 4.2.3
+
y18n: 5.0.8
+
yargs-parser: 20.2.9
dev: true
/yargs/17.7.1:
+1 -1
src/__tests__/__snapshots__/parser.test.ts.snap
···
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
exports[`print > prints the kitchen sink document like graphql.js does 1`] = `
+
exports[`parse > parses the kitchen sink document like graphql.js does 1`] = `
{
"definitions": [
{
+53
src/__tests__/error.test.ts
···
+
import { describe, it, expect } from 'vitest';
+
+
import { Kind } from '../kind';
+
import { GraphQLError as graphql_GraphQLError } from 'graphql';
+
import { GraphQLError } from '../error';
+
+
describe('GraphQLError', () => {
+
it('sorts input arguments into properties', () => {
+
const inputs = ['message', [], { body: '' }, [], [], new Error(), {}] as const;
+
+
const error = new GraphQLError(...inputs);
+
expect(error).toMatchInlineSnapshot('[GraphQLError: message]');
+
expect(error).toEqual(new (graphql_GraphQLError as any)(...inputs));
+
});
+
+
it('normalizes incoming nodes to arrays', () => {
+
const error = new GraphQLError('message', { kind: Kind.NULL });
+
expect(error.nodes).toEqual([{ kind: Kind.NULL }]);
+
});
+
+
it('allows toJSON and toString calls', () => {
+
const error = new GraphQLError('message');
+
expect(error.toString()).toEqual('message');
+
expect(error.toJSON()).toEqual({
+
name: 'GraphQLError',
+
message: 'message',
+
extensions: {},
+
locations: undefined,
+
nodes: undefined,
+
originalError: undefined,
+
path: undefined,
+
positions: undefined,
+
source: undefined,
+
});
+
});
+
+
it('normalizes extensions as expected', () => {
+
const inputs = (extensions: any, originalError = new Error()) =>
+
['message', [], { body: '' }, [], [], originalError, extensions] as const;
+
+
expect(new GraphQLError(...inputs(undefined)).extensions).toEqual({});
+
expect(new GraphQLError(...inputs({ test: true })).extensions).toEqual({ test: true });
+
+
expect(
+
new GraphQLError(...inputs({ test: true }, { extensions: { override: true } } as any))
+
.extensions
+
).toEqual({ test: true });
+
+
expect(
+
new GraphQLError(...inputs(undefined, { extensions: { override: true } } as any)).extensions
+
).toEqual({ override: true });
+
});
+
});
+390 -108
src/__tests__/parser.test.ts
···
import { parse, parseType, parseValue } from '../parser';
import { Kind } from '../kind';
-
describe('print', () => {
-
it('prints the kitchen sink document like graphql.js does', () => {
+
describe('parse', () => {
+
it('parses the kitchen sink document like graphql.js does', () => {
const sink = readFileSync(__dirname + '/../../benchmark/kitchen_sink.graphql', {
encoding: 'utf8',
});
···
expect(doc).toEqual(graphql_parse(sink, { noLocation: true }));
});
-
it('parse provides errors', () => {
+
it('parses basic documents', () => {
expect(() => parse('{')).toThrow();
+
expect(() => parse('{}x ')).toThrow();
+
expect(() => parse('{ field }')).not.toThrow();
+
expect(() => parse({ body: '{ field }' })).not.toThrow();
});
it('parses variable inline values', () => {
···
}).not.toThrow();
});
+
it('parses fragment definitions', () => {
+
expect(() => parse('fragment { test }')).toThrow();
+
expect(() => parse('fragment name { test }')).toThrow();
+
expect(() => parse('fragment name on name')).toThrow();
+
expect(() => parse('fragment Name on Type { field }')).not.toThrow();
+
});
+
+
it('parses fields', () => {
+
expect(() => parse('{ field: }')).toThrow();
+
expect(() => parse('{ alias: field() }')).toThrow();
+
+
expect(parse('{ alias: field { child } }').definitions[0]).toHaveProperty(
+
'selectionSet.selections.0',
+
{
+
kind: Kind.FIELD,
+
directives: [],
+
arguments: [],
+
alias: {
+
kind: Kind.NAME,
+
value: 'alias',
+
},
+
name: {
+
kind: Kind.NAME,
+
value: 'field',
+
},
+
selectionSet: {
+
kind: Kind.SELECTION_SET,
+
selections: [
+
{
+
kind: Kind.FIELD,
+
directives: [],
+
arguments: [],
+
name: {
+
kind: Kind.NAME,
+
value: 'child',
+
},
+
},
+
],
+
},
+
}
+
);
+
});
+
+
it('parses arguments', () => {
+
expect(() => parse('{ field() }')).toThrow();
+
expect(() => parse('{ field(name) }')).toThrow();
+
expect(() => parse('{ field(name:) }')).toThrow();
+
expect(() => parse('{ field(name: null }')).toThrow();
+
+
expect(parse('{ field(name: null) }').definitions[0]).toMatchObject({
+
kind: Kind.OPERATION_DEFINITION,
+
selectionSet: {
+
kind: Kind.SELECTION_SET,
+
selections: [
+
{
+
kind: Kind.FIELD,
+
name: {
+
kind: Kind.NAME,
+
value: 'field',
+
},
+
arguments: [
+
{
+
kind: Kind.ARGUMENT,
+
name: {
+
kind: Kind.NAME,
+
value: 'name',
+
},
+
value: {
+
kind: Kind.NULL,
+
},
+
},
+
],
+
},
+
],
+
},
+
});
+
});
+
+
it('parses directives', () => {
+
expect(() => parse('{ field @ }')).toThrow();
+
expect(() => parse('{ field @(test: null) }')).toThrow();
+
+
expect(parse('{ field @test(name: null) }')).toHaveProperty(
+
'definitions.0.selectionSet.selections.0.directives.0',
+
{
+
kind: Kind.DIRECTIVE,
+
name: {
+
kind: Kind.NAME,
+
value: 'test',
+
},
+
arguments: [
+
{
+
kind: Kind.ARGUMENT,
+
name: {
+
kind: Kind.NAME,
+
value: 'name',
+
},
+
value: {
+
kind: Kind.NULL,
+
},
+
},
+
],
+
}
+
);
+
});
+
+
it('parses inline fragments', () => {
+
expect(() => parse('{ ... on Test }')).toThrow();
+
expect(() => parse('{ ... {} }')).toThrow();
+
expect(() => parse('{ ... }')).toThrow();
+
+
expect(parse('{ ... on Test { field } }')).toHaveProperty(
+
'definitions.0.selectionSet.selections.0',
+
{
+
kind: Kind.INLINE_FRAGMENT,
+
directives: [],
+
typeCondition: {
+
kind: Kind.NAMED_TYPE,
+
name: {
+
kind: Kind.NAME,
+
value: 'Test',
+
},
+
},
+
selectionSet: expect.any(Object),
+
}
+
);
+
+
expect(parse('{ ... { field } }')).toHaveProperty('definitions.0.selectionSet.selections.0', {
+
kind: Kind.INLINE_FRAGMENT,
+
directives: [],
+
typeCondition: undefined,
+
selectionSet: expect.any(Object),
+
});
+
});
+
+
it('parses variable definitions', () => {
+
expect(() => parse('query ( { test }')).toThrow();
+
expect(() => parse('query ($var) { test }')).toThrow();
+
expect(() => parse('query ($var:) { test }')).toThrow();
+
expect(() => parse('query ($var: Int =) { test }')).toThrow();
+
+
expect(parse('query ($var: Int = 1) { test }').definitions[0]).toMatchObject({
+
kind: Kind.OPERATION_DEFINITION,
+
operation: 'query',
+
directives: [],
+
selectionSet: expect.any(Object),
+
variableDefinitions: [
+
{
+
kind: Kind.VARIABLE_DEFINITION,
+
type: {
+
kind: Kind.NAMED_TYPE,
+
name: {
+
kind: Kind.NAME,
+
value: 'Int',
+
},
+
},
+
variable: {
+
kind: Kind.VARIABLE,
+
name: {
+
kind: Kind.NAME,
+
value: 'var',
+
},
+
},
+
defaultValue: {
+
kind: Kind.INT,
+
value: '1',
+
},
+
},
+
],
+
});
+
});
+
it('creates ast', () => {
const result = parse(`
{
···
const result = parse('{ id }', { noLocation: true });
expect('loc' in result).toBe(false);
});
+
});
-
describe('parseValue', () => {
-
it('parses null value', () => {
-
const result = parseValue('null');
-
expect(result).toEqual({ kind: Kind.NULL });
+
describe('parseValue', () => {
+
it('parses basic values', () => {
+
expect(() => parseValue('')).toThrow();
+
expect(parseValue('null')).toEqual({ kind: Kind.NULL });
+
expect(parseValue({ body: 'null' })).toEqual({ kind: Kind.NULL });
+
});
+
+
it('parses list values', () => {
+
const result = parseValue('[123 "abc"]');
+
expect(result).toEqual({
+
kind: Kind.LIST,
+
values: [
+
{
+
kind: Kind.INT,
+
value: '123',
+
},
+
{
+
kind: Kind.STRING,
+
value: 'abc',
+
block: false,
+
},
+
],
});
+
});
-
it('parses list values', () => {
-
const result = parseValue('[123 "abc"]');
-
expect(result).toEqual({
-
kind: Kind.LIST,
-
values: [
-
{
-
kind: Kind.INT,
-
value: '123',
-
},
-
{
-
kind: Kind.STRING,
-
value: 'abc',
-
block: false,
-
},
-
],
-
});
+
it('parses integers', () => {
+
expect(parseValue('12')).toEqual({
+
kind: Kind.INT,
+
value: '12',
});
-
it('parses block strings', () => {
-
const result = parseValue('["""long""" "short"]');
-
expect(result).toEqual({
-
kind: Kind.LIST,
-
values: [
-
{
-
kind: Kind.STRING,
-
value: 'long',
-
block: true,
+
expect(parseValue('-12')).toEqual({
+
kind: Kind.INT,
+
value: '-12',
+
});
+
});
+
+
it('parses floats', () => {
+
expect(parseValue('12e2')).toEqual({
+
kind: Kind.FLOAT,
+
value: '12e2',
+
});
+
+
expect(parseValue('0.2E3')).toEqual({
+
kind: Kind.FLOAT,
+
value: '0.2E3',
+
});
+
+
expect(parseValue('-1.2e+3')).toEqual({
+
kind: Kind.FLOAT,
+
value: '-1.2e+3',
+
});
+
});
+
+
it('parses strings', () => {
+
expect(parseValue('"test"')).toEqual({
+
kind: Kind.STRING,
+
value: 'test',
+
block: false,
+
});
+
+
expect(parseValue('"\\t\\t"')).toEqual({
+
kind: Kind.STRING,
+
value: '\t\t',
+
block: false,
+
});
+
});
+
+
it('parses objects', () => {
+
expect(parseValue('{}')).toEqual({
+
kind: Kind.OBJECT,
+
fields: [],
+
});
+
+
expect(() => parseValue('{name}')).toThrow();
+
expect(() => parseValue('{name:}')).toThrow();
+
expect(() => parseValue('{name:null')).toThrow();
+
+
expect(parseValue('{name:null}')).toEqual({
+
kind: Kind.OBJECT,
+
fields: [
+
{
+
kind: Kind.OBJECT_FIELD,
+
name: {
+
kind: Kind.NAME,
+
value: 'name',
},
-
{
-
kind: Kind.STRING,
-
value: 'short',
-
block: false,
+
value: {
+
kind: Kind.NULL,
},
-
],
-
});
+
},
+
],
+
});
+
});
+
+
it('parses lists', () => {
+
expect(parseValue('[]')).toEqual({
+
kind: Kind.LIST,
+
values: [],
+
});
+
+
expect(() => parseValue('[')).toThrow();
+
expect(() => parseValue('[null')).toThrow();
+
+
expect(parseValue('[null]')).toEqual({
+
kind: Kind.LIST,
+
values: [
+
{
+
kind: Kind.NULL,
+
},
+
],
});
+
});
-
it('allows variables', () => {
-
const result = parseValue('{ field: $var }');
-
expect(result).toEqual({
-
kind: Kind.OBJECT,
-
fields: [
-
{
-
kind: Kind.OBJECT_FIELD,
+
it('parses block strings', () => {
+
expect(parseValue('["""long""" "short"]')).toEqual({
+
kind: Kind.LIST,
+
values: [
+
{
+
kind: Kind.STRING,
+
value: 'long',
+
block: true,
+
},
+
{
+
kind: Kind.STRING,
+
value: 'short',
+
block: false,
+
},
+
],
+
});
+
+
expect(parseValue('"""\n\n first\n second\n"""')).toEqual({
+
kind: Kind.STRING,
+
value: 'first\nsecond',
+
block: true,
+
});
+
});
+
+
it('allows variables', () => {
+
const result = parseValue('{ field: $var }');
+
expect(result).toEqual({
+
kind: Kind.OBJECT,
+
fields: [
+
{
+
kind: Kind.OBJECT_FIELD,
+
name: {
+
kind: Kind.NAME,
+
value: 'field',
+
},
+
value: {
+
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
-
value: 'field',
-
},
-
value: {
-
kind: Kind.VARIABLE,
-
name: {
-
kind: Kind.NAME,
-
value: 'var',
-
},
+
value: 'var',
},
},
-
],
-
});
+
},
+
],
});
+
});
-
it('correct message for incomplete variable', () => {
-
expect(() => {
-
return parseValue('$');
-
}).toThrow();
+
it('correct message for incomplete variable', () => {
+
expect(() => {
+
return parseValue('$');
+
}).toThrow();
+
});
+
+
it('correct message for unexpected token', () => {
+
expect(() => {
+
return parseValue(':');
+
}).toThrow();
+
});
+
});
+
+
describe('parseType', () => {
+
it('parses basic types', () => {
+
expect(() => parseType('')).toThrow();
+
expect(() => parseType('Type')).not.toThrow();
+
expect(() => parseType({ body: 'Type' })).not.toThrow();
+
});
+
+
it('throws on invalid inputs', () => {
+
expect(() => parseType('!')).toThrow();
+
expect(() => parseType('[String')).toThrow();
+
expect(() => parseType('[String!')).toThrow();
+
});
+
+
it('parses well known types', () => {
+
const result = parseType('String');
+
expect(result).toEqual({
+
kind: Kind.NAMED_TYPE,
+
name: {
+
kind: Kind.NAME,
+
value: 'String',
+
},
});
+
});
-
it('correct message for unexpected token', () => {
-
expect(() => {
-
return parseValue(':');
-
}).toThrow();
+
it('parses custom types', () => {
+
const result = parseType('MyType');
+
expect(result).toEqual({
+
kind: Kind.NAMED_TYPE,
+
name: {
+
kind: Kind.NAME,
+
value: 'MyType',
+
},
});
});
-
describe('parseType', () => {
-
it('parses well known types', () => {
-
const result = parseType('String');
-
expect(result).toEqual({
+
it('parses list types', () => {
+
const result = parseType('[MyType]');
+
expect(result).toEqual({
+
kind: Kind.LIST_TYPE,
+
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
-
value: 'String',
+
value: 'MyType',
},
-
});
+
},
});
+
});
-
it('parses custom types', () => {
-
const result = parseType('MyType');
-
expect(result).toEqual({
+
it('parses non-null types', () => {
+
const result = parseType('MyType!');
+
expect(result).toEqual({
+
kind: Kind.NON_NULL_TYPE,
+
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: 'MyType',
},
-
});
-
});
-
-
it('parses list types', () => {
-
const result = parseType('[MyType]');
-
expect(result).toEqual({
-
kind: Kind.LIST_TYPE,
-
type: {
-
kind: Kind.NAMED_TYPE,
-
name: {
-
kind: Kind.NAME,
-
value: 'MyType',
-
},
-
},
-
});
+
},
});
+
});
-
it('parses non-null types', () => {
-
const result = parseType('MyType!');
-
expect(result).toEqual({
+
it('parses nested types', () => {
+
const result = parseType('[MyType!]');
+
expect(result).toEqual({
+
kind: Kind.LIST_TYPE,
+
type: {
kind: Kind.NON_NULL_TYPE,
type: {
kind: Kind.NAMED_TYPE,
···
value: 'MyType',
},
},
-
});
-
});
-
-
it('parses nested types', () => {
-
const result = parseType('[MyType!]');
-
expect(result).toEqual({
-
kind: Kind.LIST_TYPE,
-
type: {
-
kind: Kind.NON_NULL_TYPE,
-
type: {
-
kind: Kind.NAMED_TYPE,
-
name: {
-
kind: Kind.NAME,
-
value: 'MyType',
-
},
-
},
-
},
-
});
+
},
});
});
});
+79 -6
src/__tests__/printer.test.ts
···
import { readFileSync } from 'fs';
import { parse, print as graphql_print } from 'graphql';
-
import { print } from '../printer';
+
import { print, printString, printBlockString } from '../printer';
function dedentString(string) {
const trimmedStr = string
···
return dedentString(str);
}
+
describe('printString', () => {
+
it('prints strings as expected', () => {
+
expect(printString('test')).toEqual('"test"');
+
expect(printString('\n')).toEqual('"\\n"');
+
});
+
});
+
+
describe('printBlockString', () => {
+
it('prints block strings as expected', () => {
+
expect(printBlockString('test')).toEqual('"""\ntest\n"""');
+
expect(printBlockString('\n')).toEqual('"""\n\n\n"""');
+
expect(printBlockString('"""')).toEqual('"""\n\\"""\n"""');
+
});
+
});
+
describe('print', () => {
it('prints the kitchen sink document like graphql.js does', () => {
const sink = JSON.parse(readFileSync(__dirname + '/kitchen_sink.json', { encoding: 'utf8' }));
···
});
it('prints minimal ast', () => {
-
const ast = {
-
kind: 'Field',
-
name: { kind: 'Name', value: 'foo' },
-
};
-
expect(print(ast as any)).toBe('foo');
+
expect(
+
print({
+
kind: 'Field',
+
name: { kind: 'Name', value: 'foo' },
+
} as any)
+
).toBe('foo');
+
+
expect(
+
print({
+
kind: 'Name',
+
value: 'foo',
+
} as any)
+
).toBe('foo');
+
+
expect(
+
print({
+
kind: 'Document',
+
definitions: [],
+
} as any)
+
).toBe('');
+
});
+
+
it('prints integers and floats', () => {
+
expect(
+
print({
+
kind: 'IntValue',
+
value: '12',
+
} as any)
+
).toBe('12');
+
+
expect(
+
print({
+
kind: 'FloatValue',
+
value: '12e2',
+
} as any)
+
).toBe('12e2');
+
});
+
+
it('prints lists of values', () => {
+
expect(
+
print({
+
kind: 'ListValue',
+
values: [{ kind: 'NullValue' }],
+
} as any)
+
).toBe('[null]');
+
});
+
+
it('prints types', () => {
+
expect(
+
print({
+
kind: 'ListType',
+
type: {
+
kind: 'NonNullType',
+
type: {
+
kind: 'NamedType',
+
name: {
+
kind: 'Name',
+
value: 'Type',
+
},
+
},
+
},
+
} as any)
+
).toBe('[Type!]');
});
// NOTE: The shim won't throw for invalid AST nodes
+27
src/__tests__/visitor.test.ts
···
expect(() => visit(ast, {})).not.toThrow();
});
+
it('handles noop visitor', () => {
+
const ast = parse('{ a, b }', { noLocation: true });
+
expect(() =>
+
visit(ast, {
+
enter() {
+
/*noop*/
+
},
+
})
+
).not.toThrow();
+
+
expect(() =>
+
visit(ast, {
+
enter(node) {
+
return node;
+
},
+
})
+
).not.toThrow();
+
+
expect(() =>
+
visit(ast, {
+
enter() {
+
throw new Error();
+
},
+
})
+
).toThrow();
+
});
+
it('validates path argument', () => {
const visited: any[] = [];
+1 -1
src/error.ts
···
}
toJSON(): any {
-
return { ...this };
+
return { ...this, message: this.message };
}
toString() {
+17 -18
src/parser.ts
···
}
const leadingRe = / +(?=[^\s])/y;
-
export function blockString(string: string) {
+
function blockString(string: string) {
const lines = string.split('\n');
let out = '';
let commonIndent = 0;
···
const constRe = /null|true|false/y;
const variableRe = /\$[_\w][_\d\w]*/y;
-
const intRe = /[-]?\d+/y;
-
const floatRe = /(?:[-]?\d+)?(?:\.\d+)(?:[eE][+-]?\d+)?/y;
+
const intRe = /-?\d+/y;
+
const floatPartRe = /(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
const complexStringRe = /\\/g;
const blockStringRe = /"""(?:[\s\S]+(?="""))?"""/y;
const stringRe = /"(?:[^"\r\n]+)?"/y;
···
value: match.slice(1),
},
};
-
} else if ((match = advance(floatRe))) {
-
out = {
-
kind: 'FloatValue' as Kind.FLOAT,
-
value: match,
-
};
} else if ((match = advance(intRe))) {
-
out = {
-
kind: 'IntValue' as Kind.INT,
-
value: match,
-
};
+
const intPart = match;
+
if ((match = advance(floatPartRe))) {
+
out = {
+
kind: 'FloatValue' as Kind.FLOAT,
+
value: intPart + match,
+
};
+
} else {
+
out = {
+
kind: 'IntValue' as Kind.INT,
+
value: intPart,
+
};
+
}
} else if ((match = advance(nameRe))) {
out = {
kind: 'EnumValue' as Kind.ENUM,
···
}
}
-
function type(): ast.TypeNode | undefined {
+
function type(): ast.TypeNode {
let match: ast.NameNode | ast.TypeNode | undefined;
ignored();
if (input.charCodeAt(idx) === 91 /*'['*/) {
···
ignored();
if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition');
const _type = type();
-
if (!_type) throw error('VariableDefinition');
let _defaultValue: ast.ValueNode | undefined;
if (input.charCodeAt(idx) === 61 /*'='*/) {
idx++;
···
ignored();
const definitions: ast.ExecutableDefinitionNode[] = [];
while ((match = fragmentDefinition() || operationDefinition())) definitions.push(match);
-
if (idx !== input.length) throw error('Document');
return {
kind: 'Document' as Kind.DOCUMENT,
definitions,
···
): ast.TypeNode {
input = typeof string.body === 'string' ? string.body : string;
idx = 0;
-
const _type = type();
-
if (!_type) throw error('TypeNode');
-
return _type;
+
return type();
}
+1 -3
src/visitor.ts
···
result = traverse(value[index], index, value);
path.pop();
ancestors.pop();
-
if (result === undefined) {
-
newValue.push(value[index]);
-
} else if (result === null) {
+
if (result == null) {
hasEdited = true;
} else {
hasEdited = hasEdited || result !== value[index];
+13
vitest.config.ts
···
+
import { defineConfig } from 'vitest/config';
+
+
export default defineConfig({
+
test: {
+
coverage: {
+
enabled: true,
+
provider: 'c8',
+
100: true,
+
},
+
globals: false,
+
clearMocks: true,
+
},
+
});