diff --git a/tests/bun/tests/event-source.test.ts b/tests/bun/tests/event-source.test.ts new file mode 100644 index 00000000..b31fa67c --- /dev/null +++ b/tests/bun/tests/event-source.test.ts @@ -0,0 +1,67 @@ +import { ErrorEvent } from '@standard-server/core' +import { toFetchResponse } from '@standard-server/fetch' +import { afterAll, describe, expect, it } from 'bun:test' + +let createBody: () => AsyncGenerator + +const server = Bun.serve({ + port: 0, + fetch: () => toFetchResponse({ + status: 200, + headers: {}, + body: createBody(), + }), +}) + +afterAll(() => server.stop(true)) + +/** + * Listens with EventSource until a close or error event arrives. + */ +function receive(): Promise<{ event: string, data: string }[]> { + const source = new EventSource(server.url) + const received: { event: string, data: string }[] = [] + + return new Promise((resolve) => { + for (const event of ['message', 'close', 'error']) { + source.addEventListener(event, ({ data }: any) => { + received.push({ event, data }) + + if (event !== 'message') { + source.close() + resolve(received) + } + }) + } + }) +} + +// bun-types declares EventSource, but Bun doesn't implement it yet +describe.skipIf(typeof EventSource === 'undefined')('eventSource', () => { + it('receives message and close events', async () => { + createBody = async function* () { + yield 'hello' + yield { order: 2 } + // must return a value: a close event without data is dropped by EventSource, which reconnects + return 'bye' + } + + expect(await receive()).toEqual([ + { event: 'message', data: '"hello"' }, + { event: 'message', data: '{"order":2}' }, + { event: 'close', data: '"bye"' }, + ]) + }) + + it('receives message and error events', async () => { + createBody = async function* () { + yield 'hello' + throw new ErrorEvent({ code: 'BOOM' }) + } + + expect(await receive()).toEqual([ + { event: 'message', data: '"hello"' }, + { event: 'error', data: '{"code":"BOOM"}' }, + ]) + }) +}) diff --git a/tests/deno/tests/event-source.test.ts b/tests/deno/tests/event-source.test.ts new file mode 100644 index 00000000..b40f51c0 --- /dev/null +++ b/tests/deno/tests/event-source.test.ts @@ -0,0 +1,64 @@ +import { ErrorEvent } from '@standard-server/core' +import { toFetchResponse } from '@standard-server/fetch' +import { expect } from '@std/expect' +import { afterAll, describe, it } from '@std/testing/bdd' + +let createBody: () => AsyncGenerator + +const server = Deno.serve({ port: 0, onListen: () => {} }, () => toFetchResponse({ + status: 200, + headers: {}, + body: createBody(), +})) + +afterAll(() => server.shutdown()) + +/** + * Listens with Deno's native EventSource until a close or error event arrives. + */ +function receive(): Promise<{ event: string, data: string }[]> { + const source = new EventSource(`http://localhost:${server.addr.port}`) + const received: { event: string, data: string }[] = [] + + return new Promise((resolve) => { + for (const event of ['message', 'close', 'error']) { + source.addEventListener(event, ({ data }: any) => { + received.push({ event, data }) + + if (event !== 'message') { + source.close() + resolve(received) + } + }) + } + }) +} + +describe('eventSource', () => { + it('receives message and close events', async () => { + createBody = async function* () { + yield 'hello' + yield { order: 2 } + // must return a value: a close event without data is dropped by EventSource, which reconnects + return 'bye' + } + + expect(await receive()).toEqual([ + { event: 'message', data: '"hello"' }, + { event: 'message', data: '{"order":2}' }, + { event: 'close', data: '"bye"' }, + ]) + }) + + it('receives message and error events', async () => { + createBody = async function* () { + yield 'hello' + throw new ErrorEvent({ code: 'BOOM' }) + } + + expect(await receive()).toEqual([ + { event: 'message', data: '"hello"' }, + { event: 'error', data: '{"code":"BOOM"}' }, + ]) + }) +}) diff --git a/tests/event-source.test.ts b/tests/event-source.test.ts new file mode 100644 index 00000000..7838adfa --- /dev/null +++ b/tests/event-source.test.ts @@ -0,0 +1,68 @@ +import type { AddressInfo } from 'node:net' +import * as http from 'node:http' +import { ErrorEvent } from '@standard-server/core' +import { sendStandardResponse } from '@standard-server/node' + +let createBody: () => AsyncGenerator + +const server = http.createServer((_req, res) => sendStandardResponse(res, { + status: 200, + headers: {}, + body: createBody(), +})) + +server.listen(0) + +afterAll(() => { + server.close() +}) + +/** + * Listens with EventSource until a close or error event arrives. + */ +function receive(): Promise<{ event: string, data: string }[]> { + const source = new EventSource(`http://localhost:${(server.address() as AddressInfo).port}`) + const received: { event: string, data: string }[] = [] + + return new Promise((resolve) => { + for (const event of ['message', 'close', 'error']) { + source.addEventListener(event, ({ data }: any) => { + received.push({ event, data }) + + if (event !== 'message') { + source.close() + resolve(received) + } + }) + } + }) +} + +describe.skipIf(typeof EventSource === 'undefined')('eventSource', () => { + it('receives message and close events', async () => { + createBody = async function* () { + yield 'hello' + yield { order: 2 } + // must return a value: a close event without data is dropped by EventSource, which reconnects + return 'bye' + } + + expect(await receive()).toEqual([ + { event: 'message', data: '"hello"' }, + { event: 'message', data: '{"order":2}' }, + { event: 'close', data: '"bye"' }, + ]) + }) + + it('receives message and error events', async () => { + createBody = async function* () { + yield 'hello' + throw new ErrorEvent({ code: 'BOOM' }) + } + + expect(await receive()).toEqual([ + { event: 'message', data: '"hello"' }, + { event: 'error', data: '{"code":"BOOM"}' }, + ]) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts index f5567ec0..a01eaf16 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,6 +7,8 @@ export default defineConfig(() => ({ globals: true, include: ['**/*.test.ts'], exclude: [...defaultExclude, '**/.claude/**', './tests/bun/**', './tests/deno/**'], + // exposes Node's EventSource global, which is still behind a flag (Node 20 - 26) + execArgv: ['--experimental-eventsource'], coverage: { include: ['packages/*/src/**'], exclude: ['**.test-d.*', '**.test.*', '**/*.bench.ts', './tests/bun/**', './tests/deno/**'],