@operato/graphql 10.29.27 → 10.29.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@operato/graphql",
3
3
  "description": "Webcomponent graphql-client following open-wc recommendations",
4
4
  "author": "heartyoh@hatiolab.com",
5
- "version": "10.29.27",
5
+ "version": "10.29.37",
6
6
  "type": "module",
7
7
  "main": "dist/src/index.js",
8
8
  "module": "dist/src/index.js",
@@ -23,6 +23,7 @@
23
23
  "./gql-context.js": "./dist/src/gql-context.js",
24
24
  "./graphql-client.js": "./dist/src/graphql-client.js",
25
25
  "./graphql-env.js": "./dist/src/graphql-env.js",
26
+ "./upload-progress.js": "./dist/src/upload-progress.js",
26
27
  "./json-to-graphql-query.js": "./dist/src/json-to-graphql-query.js",
27
28
  "./ox-input-graphql": "./dist/src/input/ox-input-graphql"
28
29
  },
@@ -60,7 +61,7 @@
60
61
  "dependencies": {
61
62
  "@apollo/client": "^3.6.9",
62
63
  "@operato/i18n": "^10.29.27",
63
- "@operato/utils": "^10.29.23",
64
+ "@operato/utils": "^10.29.37",
64
65
  "apollo-upload-client": "^18.0.0",
65
66
  "graphql-tag": "^2.12.6",
66
67
  "graphql-ws": "^5.14.0",
@@ -124,5 +125,5 @@
124
125
  "prettier --write"
125
126
  ]
126
127
  },
127
- "gitHead": "aa70087b5929f12cec0506c19f1aec0a83610a79"
128
+ "gitHead": "38361a65226ee9f23814747b4f0520edf42c3184"
128
129
  }
@@ -0,0 +1,156 @@
1
+ import { expect } from '@open-wc/testing'
2
+ import gql from 'graphql-tag'
3
+
4
+ import { execute } from '@apollo/client/core/index.js'
5
+
6
+ import { getClientLink } from '../src/graphql-env.js'
7
+ import { uploadContext, uploadFetch } from '../src/upload-progress.js'
8
+
9
+ /**
10
+ * Upload progress.
11
+ *
12
+ * ## Why this is tested
13
+ *
14
+ * Only XHR can tell how much of a body went out, so an upload that asks for progress leaves `fetch`.
15
+ * What must hold:
16
+ *
17
+ * ```
18
+ * unchanged a request that does not ask for progress — every query, every plain upload — still
19
+ * goes through fetch with the same arguments; no XHR is made
20
+ * progress an upload that asks gets fractions up to 1, and the same Response fetch would give
21
+ * credentials the session cookie still goes (credentials: include → withCredentials)
22
+ * abort the link's abort stops the XHR and fails like an aborted fetch
23
+ * ```
24
+ */
25
+
26
+ class FakeXHR {
27
+ static made: FakeXHR[] = []
28
+
29
+ method = ''
30
+ url = ''
31
+ withCredentials = false
32
+ headers: Record<string, string> = {}
33
+ body: any
34
+ aborted = false
35
+ status = 200
36
+ statusText = 'OK'
37
+ responseText = '{"data":{"attach":{"id":"1"}}}'
38
+ upload: { onprogress?: (e: any) => void } = {}
39
+ onload?: () => void
40
+ onerror?: () => void
41
+ onabort?: () => void
42
+
43
+ constructor() {
44
+ FakeXHR.made.push(this)
45
+ }
46
+ open(method: string, url: string) {
47
+ this.method = method
48
+ this.url = url
49
+ }
50
+ setRequestHeader(name: string, value: string) {
51
+ this.headers[name] = value
52
+ }
53
+ getAllResponseHeaders() {
54
+ return 'content-type: application/json\r\nx-one: 1\r\n'
55
+ }
56
+ send(body: any) {
57
+ this.body = body
58
+ setTimeout(() => {
59
+ this.upload.onprogress?.({ lengthComputable: true, loaded: 50, total: 100 })
60
+ this.onload?.()
61
+ })
62
+ }
63
+ abort() {
64
+ this.aborted = true
65
+ this.onabort?.()
66
+ }
67
+ }
68
+
69
+ describe('upload progress', () => {
70
+ const realXHR = window.XMLHttpRequest
71
+ const realFetch = window.fetch
72
+ let fetched: any[]
73
+
74
+ beforeEach(() => {
75
+ FakeXHR.made = []
76
+ fetched = []
77
+ ;(window as any).XMLHttpRequest = FakeXHR
78
+ window.fetch = (async (...args: any[]) => {
79
+ fetched.push(args)
80
+ return new Response('{"data":{"one":1}}', { status: 200, headers: { 'content-type': 'application/json' } })
81
+ }) as any
82
+ })
83
+
84
+ afterEach(() => {
85
+ window.XMLHttpRequest = realXHR
86
+ window.fetch = realFetch
87
+ })
88
+
89
+ it('leaves a request without progress to fetch, as it was', async () => {
90
+ const init = { method: 'POST', body: 'x', credentials: 'include' as RequestCredentials }
91
+ await uploadFetch('/graphql', init)
92
+
93
+ expect(FakeXHR.made.length).to.equal(0)
94
+ expect(fetched.length).to.equal(1)
95
+ expect(fetched[0][0]).to.equal('/graphql')
96
+ expect(fetched[0][1]).to.deep.equal(init)
97
+ })
98
+
99
+ it('reports progress and answers with the response fetch would give', async () => {
100
+ const fractions: number[] = []
101
+ const response = await uploadFetch('/graphql', {
102
+ method: 'POST',
103
+ credentials: 'include',
104
+ headers: { 'Apollo-Require-Preflight': 'true' },
105
+ body: 'form',
106
+ onUploadProgress: (fraction: number) => fractions.push(fraction)
107
+ } as any)
108
+
109
+ const xhr = FakeXHR.made[0]
110
+ expect(xhr.withCredentials, 'the session cookie stays home').to.equal(true)
111
+ expect(xhr.headers['apollo-require-preflight']).to.equal('true')
112
+ expect(xhr.body).to.equal('form')
113
+ expect(fractions).to.deep.equal([0.5, 1])
114
+
115
+ expect(response.status).to.equal(200)
116
+ expect(response.headers.get('x-one')).to.equal('1')
117
+ expect(await response.json()).to.deep.equal({ data: { attach: { id: '1' } } })
118
+ })
119
+
120
+ it('stops the upload when aborted', async () => {
121
+ const controller = new AbortController()
122
+ const sent = uploadFetch('/graphql', { method: 'POST', body: 'form', signal: controller.signal, onUploadProgress: () => {} } as any)
123
+
124
+ controller.abort()
125
+
126
+ const error = await sent.then(
127
+ () => null,
128
+ e => e
129
+ )
130
+ expect(FakeXHR.made[0].aborted).to.equal(true)
131
+ expect(error?.name).to.equal('AbortError')
132
+ })
133
+
134
+ it('sends a query through fetch and an upload with progress through XHR', async () => {
135
+ const run = (operation: any) =>
136
+ new Promise((resolve, reject) => execute(getClientLink(), operation).subscribe({ next: resolve, error: reject }))
137
+
138
+ await run({ query: gql`query { one }` })
139
+ expect(fetched.length, 'a query left fetch').to.equal(1)
140
+ expect(FakeXHR.made.length, 'a query went through XHR').to.equal(0)
141
+
142
+ const fractions: number[] = []
143
+ const file = new File(['abc'], 'a.pdf', { type: 'application/pdf' })
144
+ const result: any = await run({
145
+ query: gql`mutation ($file: Upload!) { attach(file: $file) { id } }`,
146
+ variables: { file },
147
+ context: uploadContext(fraction => fractions.push(fraction))
148
+ })
149
+
150
+ expect(fetched.length, 'the upload went through fetch').to.equal(1)
151
+ expect(FakeXHR.made.length).to.equal(1)
152
+ expect(FakeXHR.made[0].body).to.be.instanceOf(FormData)
153
+ expect(fractions).to.deep.equal([0.5, 1])
154
+ expect(result.data.attach.id).to.equal('1')
155
+ })
156
+ })