Skip to content

Commit cc3ca6b

Browse files
committed
chore: fix client.error() test
1 parent f0e063f commit cc3ca6b

File tree

2 files changed

+96
-73
lines changed

2 files changed

+96
-73
lines changed

src/browser/sse.ts

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { delay } from '~/core/delay'
1111
import { getTimestamp } from '~/core/utils/logging/getTimestamp'
1212
import { devUtils } from '~/core/utils/internal/devUtils'
1313
import { colors } from '~/core/ws/utils/attachWebSocketLogger'
14+
import { toPublicUrl } from '~/core/utils/request/toPublicUrl'
1415

1516
type EventMapConstraint = {
1617
message?: unknown
@@ -61,15 +62,38 @@ class ServerSentEventHandler<
6162
constructor(path: Path, resolver: ServerSentEventResolver<EventMap, any>) {
6263
invariant(
6364
typeof EventSource !== 'undefined',
64-
'Failed to construct a Server-Sent Event handler for path "%s": your environment does not support the EventSource API',
65+
'Failed to construct a Server-Sent Event handler for path "%s": the EventSource API is not supported in this environment',
6566
path,
6667
)
6768

6869
const clientEmitter = new Emitter<ServerSentEventClientEventMap>()
6970

70-
super('GET', path, (info) => {
71+
super('GET', path, async (info) => {
72+
const responseInit: ResponseInit = {
73+
headers: {
74+
'content-type': 'text/event-stream',
75+
'cache-control': 'no-cache',
76+
connection: 'keep-alive',
77+
},
78+
}
79+
80+
/**
81+
* @note Log the intercepted request early.
82+
* Normally, the `this.log()` method is called when the handler returns a response.
83+
* For SSE, call that method earlier so the logs are in correct order.
84+
*/
85+
await super.log({
86+
request: info.request,
87+
/**
88+
* @note Construct a placeholder response since SSE response
89+
* is being streamed and cannot be cloned/consumed for logging.
90+
*/
91+
response: new Response('[streaming]', responseInit),
92+
})
93+
this.#attachClientLogger(info.request, clientEmitter)
94+
7195
const stream = new ReadableStream({
72-
start(controller) {
96+
async start(controller) {
7397
const client = new ServerSentEventClient<EventMap>({
7498
controller,
7599
emitter: clientEmitter,
@@ -79,24 +103,16 @@ class ServerSentEventHandler<
79103
client,
80104
})
81105

82-
resolver({
106+
await resolver({
83107
...info,
84108
client,
85109
server,
86110
})
87111
},
88112
})
89113

90-
return new Response(stream, {
91-
headers: {
92-
'content-type': 'text/event-stream',
93-
'cache-control': 'no-cache',
94-
connection: 'keep-alive',
95-
},
96-
})
114+
return new Response(stream, responseInit)
97115
})
98-
99-
this.attachClientLogger(clientEmitter)
100116
}
101117

102118
predicate(args: {
@@ -110,30 +126,27 @@ class ServerSentEventHandler<
110126
return super.predicate(args)
111127
}
112128

113-
async log(args: { request: Request; response: Response }): Promise<void> {
114-
super.log({
115-
request: args.request,
116-
117-
/**
118-
* @note Construct a placeholder response since SSE response
119-
* is being streamed and cannot be cloned/consumed at this point.
120-
* This also allows us to rely on the same logging logic as in HTTP handlers.
121-
*/
122-
response: new Response('[streaming]', {
123-
status: args.response.status,
124-
statusText: args.response.statusText,
125-
headers: args.response.headers,
126-
}),
127-
})
129+
async log(_args: { request: Request; response: Response }): Promise<void> {
130+
/**
131+
* @note Skip the default `this.log()` logic so that when this handler is logged
132+
* upon handling the request, nothing is printed (we log SSE requests early).
133+
*/
134+
return
128135
}
129136

130-
private attachClientLogger(
137+
#attachClientLogger(
138+
request: Request,
131139
emitter: Emitter<ServerSentEventClientEventMap>,
132140
): void {
141+
const publicUrl = toPublicUrl(request.url)
142+
133143
/* eslint-disable no-console */
134144
emitter.on('message', (payload) => {
135145
console.groupCollapsed(
136-
devUtils.formatMessage(`${getTimestamp()} %c⇣%c ${payload.event}`),
146+
devUtils.formatMessage(
147+
`${getTimestamp()} SSE %s %c⇣%c ${payload.event}`,
148+
),
149+
publicUrl,
137150
`color:${colors.mocked}`,
138151
'color:inherit',
139152
)
@@ -142,19 +155,25 @@ class ServerSentEventHandler<
142155
})
143156

144157
emitter.on('error', () => {
145-
console.log(
146-
devUtils.formatMessage(`${getTimestamp()} %c\u00D7%c error`),
158+
console.groupCollapsed(
159+
devUtils.formatMessage(`${getTimestamp()} SSE %s %c\u00D7%c error`),
160+
publicUrl,
147161
`color: ${colors.system}`,
148162
'color:inherit',
149163
)
164+
console.log('Handler:', this)
165+
console.groupEnd()
150166
})
151167

152168
emitter.on('close', () => {
153-
console.log(
154-
devUtils.formatMessage(`${getTimestamp()} %c■%c close`),
169+
console.groupCollapsed(
170+
devUtils.formatMessage(`${getTimestamp()} SSE %s %c■%c close`),
171+
publicUrl,
155172
`colors:${colors.system}`,
156173
'color:inherit',
157174
)
175+
console.log('Handler:', this)
176+
console.groupEnd()
158177
})
159178
/* eslint-enable no-console */
160179
}
@@ -200,11 +219,11 @@ class ServerSentEventClient<
200219
> {
201220
#encoder: TextEncoder
202221
#controller: ReadableStreamDefaultController
203-
#emitter?: Emitter<ServerSentEventClientEventMap>
222+
#emitter: Emitter<ServerSentEventClientEventMap>
204223

205224
constructor(args: {
206225
controller: ReadableStreamDefaultController
207-
emitter?: Emitter<ServerSentEventClientEventMap>
226+
emitter: Emitter<ServerSentEventClientEventMap>
208227
}) {
209228
this.#encoder = new TextEncoder()
210229
this.#controller = args.controller
@@ -260,24 +279,29 @@ class ServerSentEventClient<
260279
this.error()
261280
return
262281
}
282+
283+
if (event.type === 'close') {
284+
this.close()
285+
return
286+
}
263287
}
264288

265289
/**
266290
* Errors the underlying `EventSource`, closing the connection with an error.
267-
* Erroring the connection with an error will not trigger a reconnect from the client.
291+
* This is equivalent to aborting the connection and will produce a `TypeError: Failed to fetch`
292+
* error.
268293
*/
269294
public error(): void {
270295
this.#controller.error()
271-
this.#emitter?.emit('error')
296+
this.#emitter.emit('error')
272297
}
273298

274299
/**
275300
* Closes the underlying `EventSource`, closing the connection.
276-
* Closing the connection will trigger a reconnect from the client.
277301
*/
278302
public close(): void {
279303
this.#controller.close()
280-
this.#emitter?.emit('close')
304+
this.#emitter.emit('close')
281305
}
282306

283307
#sendRetry(retry: number): void {
@@ -306,7 +330,7 @@ class ServerSentEventClient<
306330
frames.push('', '')
307331
this.#controller.enqueue(this.#encoder.encode(frames.join('\n')))
308332

309-
this.#emitter?.emit('message', {
333+
this.#emitter.emit('message', {
310334
id: payload.id,
311335
event: payload.event?.toString() || 'message',
312336
data: payload.data,

test/browser/sse-api/sse.client.send.test.ts

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ test('sends a mock message event', async ({ loadExample, page }) => {
2929
})
3030

3131
const message = await page.evaluate(() => {
32-
return new Promise((resolve, reject) => {
32+
return new Promise<string>((resolve, reject) => {
3333
const source = new EventSource('http://localhost/stream')
3434
source.onerror = () => reject()
3535

@@ -51,18 +51,21 @@ test('sends a mock custom event', async ({ loadExample, page }) => {
5151
const { setupWorker, sse } = window.msw
5252

5353
const worker = setupWorker(
54-
sse('http://localhost/stream', ({ client }) => {
55-
client.send({
56-
event: 'userconnect',
57-
data: { username: 'john' },
58-
})
59-
}),
54+
sse<{ userconnect: { username: string } }>(
55+
'http://localhost/stream',
56+
({ client }) => {
57+
client.send({
58+
event: 'userconnect',
59+
data: { username: 'john' },
60+
})
61+
},
62+
),
6063
)
6164
await worker.start()
6265
})
6366

6467
const message = await page.evaluate(() => {
65-
return new Promise((resolve, reject) => {
68+
return new Promise<string>((resolve, reject) => {
6669
const source = new EventSource('http://localhost/stream')
6770
source.addEventListener('userconnect', (event) => {
6871
resolve(`${event.type}:${event.data}`)
@@ -86,19 +89,22 @@ test('sends a mock message event with custom id', async ({
8689
const { setupWorker, sse } = window.msw
8790

8891
const worker = setupWorker(
89-
sse('http://localhost/stream', ({ client }) => {
90-
client.send({
91-
id: 'abc-123',
92-
event: 'userconnect',
93-
data: { username: 'john' },
94-
})
95-
}),
92+
sse<{ userconnect: { username: string } }>(
93+
'http://localhost/stream',
94+
({ client }) => {
95+
client.send({
96+
id: 'abc-123',
97+
event: 'userconnect',
98+
data: { username: 'john' },
99+
})
100+
},
101+
),
96102
)
97103
await worker.start()
98104
})
99105

100106
const message = await page.evaluate(() => {
101-
return new Promise((resolve, reject) => {
107+
return new Promise<string>((resolve, reject) => {
102108
const source = new EventSource('http://localhost/stream')
103109
source.addEventListener('userconnect', (event) => {
104110
resolve(`${event.type}:${event.lastEventId}:${event.data}`)
@@ -110,41 +116,34 @@ test('sends a mock message event with custom id', async ({
110116
expect(message).toBe('userconnect:abc-123:{"username":"john"}')
111117
})
112118

113-
test('errors the connected source', async ({ loadExample, page, waitFor }) => {
119+
test('errors the connected source', async ({ loadExample, page }) => {
114120
await loadExample(EXAMPLE_URL, {
115121
skipActivation: true,
116122
})
117123

118-
const pageErrors: Array<string> = []
119-
page.on('pageerror', (error) => {
120-
pageErrors.push(error.message)
121-
})
122-
123124
await page.evaluate(async () => {
124125
const { setupWorker, sse } = window.msw
125126

126127
const worker = setupWorker(
127-
sse('http://localhost/stream', ({ client }) => {
128+
sse('http://localhost/stream-error', ({ client }) => {
128129
queueMicrotask(() => client.error())
129130
}),
130131
)
131132
await worker.start()
132133
})
133134

134135
const readyState = await page.evaluate(() => {
135-
return new Promise((resolve) => {
136-
const source = new EventSource('http://localhost/stream')
137-
source.addEventListener('error', (event) => {
138-
resolve(source.readyState)
139-
})
136+
return new Promise<number>((resolve) => {
137+
const source = new EventSource('http://localhost/stream-error')
138+
source.onerror = () => resolve(source.readyState)
139+
source.onopen = () => console.log('OPEN?')
140140
})
141141
})
142142

143143
// EventSource must be closed.
144144
expect(readyState).toBe(2)
145145

146-
await waitFor(() => {
147-
// Must error with "Failed to fetch" (default EventSource behavior).
148-
expect(pageErrors).toContain('Failed to fetch')
149-
})
146+
/**
147+
* @note That erroring the stream does not throw any errors.
148+
*/
150149
})

0 commit comments

Comments
 (0)