Skip to content

Commit 3eb4ab8

Browse files
authored
Run tests with Bun (#6863)
* Run tests with Bun * Hmm * Fix tests * Hmm * Fix Unit tests * Clear lest
1 parent fce1733 commit 3eb4ab8

36 files changed

+1692
-1651
lines changed

.github/workflows/tests.yml

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,50 @@ jobs:
9494
- name: Build
9595
run: yarn build
9696
- name: Unit Tests
97-
run: yarn test --ci
97+
uses: nick-fields/retry@v3
98+
with:
99+
timeout_minutes: 10
100+
max_attempts: 5
101+
command: yarn test --ci
98102
- name: Leak Tests
99103
uses: nick-fields/retry@v3
100104
with:
101105
timeout_minutes: 10
102106
max_attempts: 5
103107
command: yarn test:leaks --ci
108+
test-bun:
109+
name: Unit Test on Bun (${{matrix.os}}) and GraphQL v${{matrix.graphql_version}}
110+
runs-on: ubuntu-latest
111+
strategy:
112+
fail-fast: false
113+
matrix:
114+
os: [ubuntu-latest] # remove windows to speed up the tests
115+
graphql_version:
116+
- 15
117+
- 16
118+
steps:
119+
- name: Checkout Master
120+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
121+
122+
- name: Setup env
123+
uses: the-guild-org/shared-config/setup@main
124+
with:
125+
nodeVersion: 23
126+
127+
- name: Use GraphQL v${{matrix.graphql_version}}
128+
run: node ./scripts/match-graphql.js ${{matrix.graphql_version}}
129+
- name: Install Dependencies using Yarn
130+
run: yarn install --ignore-engines && git checkout yarn.lock
131+
- name: Build
132+
run: yarn build
133+
- name: Unit Tests
134+
uses: nick-fields/retry@v3
135+
with:
136+
timeout_minutes: 10
137+
max_attempts: 5
138+
command: yarn test:bun --ci --reporter=junit --reporter-outfile=./junit.xml
139+
- name: Publish Test Report
140+
uses: mikepenz/action-junit-report@v5
141+
if: success() || failure() # always run even if the previous step fails
142+
with:
143+
report_paths: './junit.xml'

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"prettier:check": "prettier --cache --ignore-path .gitignore --ignore-path .prettierignore --check .",
4141
"release": "changeset publish",
4242
"test": "jest --no-watchman",
43+
"test:bun": "bun test",
4344
"test:leaks": "cross-env \"LEAK_TEST=1\" jest --no-watchman --detectOpenHandles --detectLeaks --forceExit",
4445
"ts:check": "tsc --noEmit"
4546
},
@@ -57,6 +58,7 @@
5758
"@typescript-eslint/parser": "8.20.0",
5859
"babel-jest": "29.7.0",
5960
"bob-the-bundler": "7.0.1",
61+
"bun": "^1.1.43",
6062
"chalk": "5.4.1",
6163
"concurrently": "9.1.2",
6264
"cross-env": "7.0.3",

packages/documents/tests/print-executable-graphql-document.spec.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('printExecutableGraphQLDocument', () => {
1111
}
1212
`);
1313
const outputStr = printExecutableGraphQLDocument(inputDocument);
14-
expect(outputStr).toMatchInlineSnapshot(`"query A { a b c }"`);
14+
expect(outputStr).toBe(`query A { a b c }`);
1515
});
1616

1717
test('fragments are always before query operations', () => {
@@ -37,8 +37,8 @@ describe('printExecutableGraphQLDocument', () => {
3737
}
3838
`);
3939
const outputStr = printExecutableGraphQLDocument(inputDocument);
40-
expect(outputStr).toMatchInlineSnapshot(
41-
`"fragment A on Query { a b c } fragment B on Query { a b c } query A { a b c ...A ...B }"`,
40+
expect(outputStr).toBe(
41+
`fragment A on Query { a b c } fragment B on Query { a b c } query A { a b c ...A ...B }`,
4242
);
4343
});
4444

@@ -54,7 +54,7 @@ describe('printExecutableGraphQLDocument', () => {
5454
}
5555
`);
5656
const outputStr = printExecutableGraphQLDocument(inputDocument);
57-
expect(outputStr).toMatchInlineSnapshot(`"query A { ... on Query { a } ... on Query { b } }"`);
57+
expect(outputStr).toBe(`query A { ... on Query { a } ... on Query { b } }`);
5858
});
5959

6060
test('inline fragments are sorted alphabetically based on the deep selection set', () => {
@@ -79,8 +79,8 @@ describe('printExecutableGraphQLDocument', () => {
7979
}
8080
`);
8181
const outputStr = printExecutableGraphQLDocument(inputDocument);
82-
expect(outputStr).toMatchInlineSnapshot(
83-
`"fragment B on Query { c } query A { ... on Query { a { a ...B } } ... on Query { a { b ...B } } }"`,
82+
expect(outputStr).toBe(
83+
`fragment B on Query { c } query A { ... on Query { a { a ...B } } ... on Query { a { b ...B } } }`,
8484
);
8585
});
8686

@@ -105,8 +105,6 @@ describe('printExecutableGraphQLDocument', () => {
105105
}
106106
`);
107107
const outputStr = printExecutableGraphQLDocument(inputDocument);
108-
expect(outputStr).toMatchInlineSnapshot(
109-
`"mutation A { ... on Mutation { c b d } c { d e { a b } f } b a }"`,
110-
);
108+
expect(outputStr).toBe(`mutation A { ... on Mutation { c b d } c { d e { a b } f } b a }`);
111109
});
112110
});

packages/executor/src/__testUtils__/expectJSON.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { isObjectLike } from '@graphql-tools/utils';
2+
// eslint-disable-next-line import/no-extraneous-dependencies
3+
import { expect } from '@jest/globals';
24

35
/**
46
* Creates an object map with the same keys as `map` and values generated by

packages/executor/src/execution/__tests__/abort-signal.test.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ describe('Abort Signal', () => {
1818
expect(spy.mock.calls.length).toBeLessThanOrEqual(1);
1919
});
2020
it('should stop the subscription', async () => {
21-
expect.assertions(3);
2221
let stopped = false;
2322
const schema = makeExecutableSchema({
2423
typeDefs: /* GraphQL */ `
@@ -127,7 +126,7 @@ describe('Abort Signal', () => {
127126
const $next = iterator.next();
128127
await rootResolverGotInvokedD.promise;
129128
controller.abort();
130-
await expect($next).rejects.toMatchInlineSnapshot(`DOMException {}`);
129+
await expect($next).rejects.toBeInstanceOf(DOMException);
131130
expect(aResolverGotInvoked).toEqual(false);
132131
});
133132
it('should stop the serial mutation execution', async () => {
@@ -174,7 +173,7 @@ describe('Abort Signal', () => {
174173
`),
175174
signal: controller.signal,
176175
});
177-
expect(result$).rejects.toMatchInlineSnapshot(`DOMException {}`);
176+
expect(result$).rejects.toBeInstanceOf(DOMException);
178177
expect(didInvokeFirstFn).toBe(true);
179178
expect(didInvokeSecondFn).toBe(true);
180179
expect(didInvokeThirdFn).toBe(false);
@@ -224,7 +223,7 @@ describe('Abort Signal', () => {
224223
`),
225224
signal: controller.signal,
226225
});
227-
await expect(result$).rejects.toMatchInlineSnapshot(`DOMException {}`);
226+
await expect(result$).rejects.toBeInstanceOf(DOMException);
228227
expect(isAborted).toEqual(true);
229228
});
230229
it('stops pending stream execution for incremental delivery (@stream)', async () => {
@@ -285,7 +284,7 @@ describe('Abort Signal', () => {
285284

286285
const next$ = iter.next();
287286
controller.abort();
288-
await expect(next$).rejects.toMatchInlineSnapshot(`DOMException {}`);
287+
await expect(next$).rejects.toBeInstanceOf(DOMException);
289288
expect(isReturnInvoked).toEqual(true);
290289
});
291290
it('stops pending stream execution for parallel sources incremental delivery (@stream)', async () => {
@@ -365,7 +364,7 @@ describe('Abort Signal', () => {
365364

366365
const next$ = iter.next();
367366
controller.abort();
368-
await expect(next$).rejects.toMatchInlineSnapshot(`DOMException {}`);
367+
await expect(next$).rejects.toBeInstanceOf(DOMException);
369368
expect(isReturn1Invoked).toEqual(true);
370369
expect(isReturn2Invoked).toEqual(true);
371370
});
@@ -429,19 +428,17 @@ describe('Abort Signal', () => {
429428

430429
const iterator = result[Symbol.asyncIterator]();
431430
const next = await iterator.next();
432-
expect(next.value).toMatchInlineSnapshot(`
433-
{
434-
"data": {
435-
"root": {},
436-
},
437-
"hasNext": true,
438-
}
439-
`);
431+
expect(next.value).toEqual({
432+
data: {
433+
root: {},
434+
},
435+
hasNext: true,
436+
});
440437
const next$ = iterator.next();
441438
await aResolverGotInvokedD.promise;
442439
controller.abort();
443440
requestGotCancelledD.resolve();
444-
await expect(next$).rejects.toThrow('This operation was aborted');
441+
await expect(next$).rejects.toThrow(/operation was aborted/);
445442
expect(bResolverGotInvoked).toBe(false);
446443
});
447444
it('stops promise execution', async () => {
@@ -472,7 +469,7 @@ describe('Abort Signal', () => {
472469

473470
expect(result$).toBeInstanceOf(Promise);
474471
controller.abort();
475-
await expect(result$).rejects.toMatchInlineSnapshot(`DOMException {}`);
472+
await expect(result$).rejects.toBeInstanceOf(DOMException);
476473
});
477474
it('does not even try to execute if the signal is already aborted', async () => {
478475
let resolverGotInvoked = false;
@@ -502,7 +499,7 @@ describe('Abort Signal', () => {
502499
`),
503500
signal: controller.signal,
504501
}),
505-
).toThrowErrorMatchingInlineSnapshot(`"This operation was aborted"`);
502+
).toThrow(/operation was aborted/);
506503
expect(resolverGotInvoked).toBe(false);
507504
});
508505
});

packages/executor/src/execution/__tests__/error-handling.test.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,16 @@ describe('Error Handling', () => {
3636
});
3737
if (globalThis.fetch != null) {
3838
let server: Server;
39-
afterEach(done => {
39+
afterEach(async () => {
4040
if (!server) {
41-
done();
4241
return;
4342
}
44-
server.closeAllConnections();
45-
server.close(done);
43+
if (!globalThis.Bun) {
44+
server.closeAllConnections();
45+
}
46+
await new Promise<void>((resolve, reject) => {
47+
server.close(err => (err ? reject(err) : resolve()));
48+
});
4649
});
4750
it('handles undici fetch JSON parsing errors', async () => {
4851
server = createServer((_, res) => {
@@ -80,7 +83,9 @@ describe('Error Handling', () => {
8083
throw new Error('Expected a result, but got an async iterable');
8184
}
8285
const errorMessage = result.errors?.[0]?.message;
83-
if (process.versions['node'].startsWith('18.')) {
86+
if (globalThis.Bun) {
87+
expect(errorMessage).toBeTruthy();
88+
} else if (process.versions['node'].startsWith('18.')) {
8489
expect(errorMessage).toBe('Unexpected end of JSON input');
8590
} else {
8691
expect(errorMessage).toContain(

packages/executors/apollo-link/tests/apollo-link.spec.ts

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe('Apollo Link', () => {
5858
const client = new ApolloClient({
5959
link: new ExecutorLink(
6060
buildHTTPExecutor({
61+
endpoint: `http://localhost${yoga.graphqlEndpoint}`,
6162
fetch: yoga.fetch,
6263
File: yoga.fetchAPI.File,
6364
FormData: yoga.fetchAPI.FormData,
@@ -87,34 +88,37 @@ describe('Apollo Link', () => {
8788
hello: 'Hello Apollo Client!',
8889
});
8990
});
90-
testIf(!process.env['LEAK_TEST'])('should handle subscriptions correctly', async () => {
91-
expect.assertions(5);
92-
const observable = client.subscribe({
93-
query: parse(/* GraphQL */ `
94-
subscription Time {
95-
time
96-
}
97-
`),
98-
});
99-
const collectedValues: string[] = [];
100-
let i = 0;
101-
await new Promise<void>((resolve, reject) => {
102-
const subscription = observable.subscribe((result: FetchResult) => {
103-
collectedValues.push(result.data?.['time']);
104-
i++;
105-
if (i > 2) {
106-
subscription.unsubscribe();
107-
resolve();
108-
}
109-
}, reject);
110-
});
111-
expect(collectedValues.length).toBe(3);
112-
expect(i).toBe(3);
113-
const now = new Date();
114-
for (const value of collectedValues) {
115-
expect(new Date(value).getFullYear()).toBe(now.getFullYear());
116-
}
117-
});
91+
testIf(!process.env['LEAK_TEST'] && !globalThis.Bun)(
92+
'should handle subscriptions correctly',
93+
async () => {
94+
expect.assertions(5);
95+
const observable = client.subscribe({
96+
query: parse(/* GraphQL */ `
97+
subscription Time {
98+
time
99+
}
100+
`),
101+
});
102+
const collectedValues: string[] = [];
103+
let i = 0;
104+
await new Promise<void>((resolve, reject) => {
105+
const subscription = observable.subscribe((result: FetchResult) => {
106+
collectedValues.push(result.data?.['time']);
107+
i++;
108+
if (i > 2) {
109+
subscription.unsubscribe();
110+
resolve();
111+
}
112+
}, reject);
113+
});
114+
expect(collectedValues.length).toBe(3);
115+
expect(i).toBe(3);
116+
const now = new Date();
117+
for (const value of collectedValues) {
118+
expect(new Date(value).getFullYear()).toBe(now.getFullYear());
119+
}
120+
},
121+
);
118122
it('should handle file uploads correctly', async () => {
119123
const result = await client.mutate({
120124
mutation: parse(/* GraphQL */ `
@@ -131,20 +135,23 @@ describe('Apollo Link', () => {
131135
readFile: 'Hello World',
132136
});
133137
});
134-
it('should complete subscription even while waiting for events', async () => {
135-
const observable = client.subscribe({
136-
query: parse(/* GraphQL */ `
137-
subscription Ping {
138-
ping
139-
}
140-
`),
141-
});
142-
const sub = observable.subscribe({
143-
next: () => {
144-
// noop
145-
},
146-
});
147-
globalThis.setTimeout(() => sub.unsubscribe(), 0);
148-
await waitForPingStop;
149-
});
138+
testIf(!globalThis.Bun)(
139+
'should complete subscription even while waiting for events',
140+
async () => {
141+
const observable = client.subscribe({
142+
query: parse(/* GraphQL */ `
143+
subscription Ping {
144+
ping
145+
}
146+
`),
147+
});
148+
const sub = observable.subscribe({
149+
next: () => {
150+
// noop
151+
},
152+
});
153+
globalThis.setTimeout(() => sub.unsubscribe(), 0);
154+
await waitForPingStop;
155+
},
156+
);
150157
});

0 commit comments

Comments
 (0)