diff --git a/.changeset/sharp-carrots-hunt.md b/.changeset/sharp-carrots-hunt.md new file mode 100644 index 00000000000..acf6e6fe889 --- /dev/null +++ b/.changeset/sharp-carrots-hunt.md @@ -0,0 +1,7 @@ +--- +'@graphql-tools/executor-urql-exchange': patch +'@graphql-tools/executor-apollo-link': patch +'@graphql-tools/utils': patch +--- + +Improvements for `fakePromise` so it can be used without params to create a `void` Promise diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5ac937efeaa..3dda7def11b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -4,6 +4,9 @@ on: branches: - master +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + jobs: dependencies: uses: the-guild-org/shared-config/.github/workflows/changesets-dependencies.yaml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa99fde38c7..c8fa7a84a2b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,9 @@ on: branches: - master +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + jobs: stable: permissions: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0b88fb73450..e48ab18e716 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,10 @@ env: NODE_OPTIONS: '--max-old-space-size=8192' CI: true +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: branches: @@ -12,37 +16,9 @@ on: pull_request: jobs: - prettier-check: - name: 🧹 Prettier Check - runs-on: ubuntu-latest - steps: - - name: Checkout Master - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - - name: Setup env - uses: the-guild-org/shared-config/setup@main - with: - nodeVersion: 22 - - - name: Prettier Check - run: yarn prettier:check - lint: - name: Lint - uses: the-guild-org/shared-config/.github/workflows/lint.yml@main - with: - script: yarn ci:lint - secrets: - githubToken: ${{ secrets.GITHUB_TOKEN }} - - build: - name: Type Check on GraphQL v${{matrix.graphql_version}} + typecheck-15: + name: Type Check on GraphQL v15 runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - graphql_version: - - 15 - - 16 steps: - name: Checkout Master uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -52,25 +28,27 @@ jobs: with: nodeVersion: 22 - - name: Use GraphQL v${{matrix.graphql_version}} - run: node ./scripts/match-graphql.js ${{matrix.graphql_version}} + - name: Use GraphQL v15 + run: node ./scripts/match-graphql.js 15 - name: Install Dependencies using Yarn run: yarn install --ignore-engines && git checkout yarn.lock - name: Type Check run: yarn ts:check - test_esm: - name: ESM Test + check: + name: Full Check on GraphQL v16 runs-on: ubuntu-latest steps: - name: Checkout Master uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Setup env uses: the-guild-org/shared-config/setup@main with: nodeVersion: 22 - - - name: Build Packages + - name: Prettier + run: yarn prettier:check + - name: Lint + run: yarn lint + - name: Build run: yarn build - name: Test ESM and CJS integrity run: yarn bob check @@ -83,7 +61,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] # remove windows to speed up the tests - node-version: [18, 20, 22] + node-version: [18, 20, 22, 23] graphql_version: - 15 - 16 @@ -113,34 +91,13 @@ jobs: hashFiles('yarn.lock') }} restore-keys: | ${{ runner.os }}-${{matrix.node-version}}-${{matrix.graphql_version}}-jest- - - name: Test - if: ${{ matrix.node-version >= 20 }} + - name: Build + run: yarn build + - name: Unit Tests run: yarn test --ci - - name: Test - if: ${{ matrix.node-version < 20 }} + - name: Leak Tests uses: nick-fields/retry@v3 with: timeout_minutes: 10 max_attempts: 5 command: yarn test:leaks --ci - - test_browser: - name: Browser Test - runs-on: ubuntu-latest - steps: - - name: Checkout Master - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Setup env - uses: the-guild-org/shared-config/setup@main - with: - nodeVersion: 22 - - name: Setup Chrome - uses: browser-actions/setup-chrome@v1 - - name: Build Packages - run: yarn build - - name: Test - uses: nick-fields/retry@v3 - with: - timeout_minutes: 10 - max_attempts: 5 - command: TEST_BROWSER=true yarn jest --no-watchman --ci browser diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index d68977d99d5..183cf0550d3 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -6,6 +6,9 @@ on: - master pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + jobs: deployment: runs-on: ubuntu-latest diff --git a/jest.config.js b/jest.config.js index 7b818407cf3..f5d0ddef9f4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -36,6 +36,7 @@ externalToolsPackages.forEach(mod => { }); module.exports = { + displayName: process.env.LEAK_TEST ? 'Leak Test' : 'Unit Test', testEnvironment: 'node', rootDir: ROOT_DIR, prettierPath: null, // disable prettier for inline snapshots diff --git a/package.json b/package.json index bf7d37caa6a..94e95fea468 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "prettier:check": "prettier --cache --ignore-path .prettierignore --check .", "release": "changeset publish", "test": "jest --no-watchman", - "test:leaks": "cross-env \"LEAK_TEST=1\" jest --no-watchman --detectOpenHandles --detectLeaks --logHeapUsage", + "test:leaks": "cross-env \"LEAK_TEST=1\" jest --no-watchman --detectOpenHandles --detectLeaks --forceExit", "ts:check": "tsc --noEmit" }, "devDependencies": { diff --git a/packages/executor/src/execution/__tests__/simplePubSub-test.ts b/packages/executor/src/execution/__tests__/simplePubSub-test.ts index 54a736e2320..0e3f35bc166 100644 --- a/packages/executor/src/execution/__tests__/simplePubSub-test.ts +++ b/packages/executor/src/execution/__tests__/simplePubSub-test.ts @@ -1,4 +1,4 @@ -import { SimplePubSub } from './simplePubSub.js'; +import { SimplePubSub } from '../../../../testing/simplePubSub.js'; describe('SimplePubSub', () => { it('subscribe async-iterator mock', async () => { diff --git a/packages/executor/src/execution/__tests__/subscribe.test.ts b/packages/executor/src/execution/__tests__/subscribe.test.ts index bf17d40dae7..ad5c737df49 100644 --- a/packages/executor/src/execution/__tests__/subscribe.test.ts +++ b/packages/executor/src/execution/__tests__/subscribe.test.ts @@ -12,9 +12,9 @@ import { ExecutionResult, isAsyncIterable, isPromise, MaybePromise } from '@grap import { expectJSON } from '../../__testUtils__/expectJSON.js'; import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js'; import { assertAsyncIterable } from '../../../../loaders/url/tests/test-utils.js'; +import { SimplePubSub } from '../../../../testing/simplePubSub.js'; import { ExecutionArgs, subscribe } from '../execute.js'; import { normalizedExecutor } from '../normalizedExecutor.js'; -import { SimplePubSub } from './simplePubSub.js'; interface Email { from: string; diff --git a/packages/executors/apollo-link/src/index.ts b/packages/executors/apollo-link/src/index.ts index c1b7b8685e2..36cf74b0574 100644 --- a/packages/executors/apollo-link/src/index.ts +++ b/packages/executors/apollo-link/src/index.ts @@ -1,5 +1,5 @@ import * as apolloImport from '@apollo/client'; -import { ExecutionRequest, Executor, isAsyncIterable } from '@graphql-tools/utils'; +import { Executor, fakePromise, isAsyncIterable } from '@graphql-tools/utils'; const apollo: typeof apolloImport = (apolloImport as any)?.default ?? apolloImport; @@ -8,34 +8,37 @@ function createApolloRequestHandler(executor: Executor): apolloImport.RequestHan operation: apolloImport.Operation, ): apolloImport.Observable { return new apollo.Observable(observer => { - Promise.resolve().then(async () => { - const executionRequest: ExecutionRequest = { - document: operation.query, - variables: operation.variables, - operationName: operation.operationName, - extensions: operation.extensions, - context: operation.getContext(), - }; - try { - const results = await executor(executionRequest); + fakePromise() + .then(() => + executor({ + document: operation.query, + variables: operation.variables, + operationName: operation.operationName, + extensions: operation.extensions, + context: operation.getContext(), + }), + ) + .then(results => { if (isAsyncIterable(results)) { - for await (const result of results) { - if (observer.closed) { - return; + return fakePromise().then(async () => { + for await (const result of results) { + if (observer.closed) { + return; + } + observer.next(result); } - observer.next(result); - } - observer.complete(); + observer.complete(); + }); } else if (!observer.closed) { observer.next(results); observer.complete(); } - } catch (e) { + }) + .catch(e => { if (!observer.closed) { observer.error(e); } - } - }); + }); }); }; } diff --git a/packages/executors/apollo-link/tests/browser-apollo-link.spec.ts b/packages/executors/apollo-link/tests/apollo-link.spec.ts similarity index 87% rename from packages/executors/apollo-link/tests/browser-apollo-link.spec.ts rename to packages/executors/apollo-link/tests/apollo-link.spec.ts index 25767d9d588..9cf46389a97 100644 --- a/packages/executors/apollo-link/tests/browser-apollo-link.spec.ts +++ b/packages/executors/apollo-link/tests/apollo-link.spec.ts @@ -1,14 +1,12 @@ +import { setTimeout } from 'timers/promises'; import { parse } from 'graphql'; import { createSchema, createYoga } from 'graphql-yoga'; import { ApolloClient, FetchResult, InMemoryCache } from '@apollo/client/core'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; +import { testIf } from '../../../testing/utils.js'; import { ExecutorLink } from '../src/index.js'; describe('Apollo Link', () => { - if (!process.env['TEST_BROWSER']) { - it('skips', () => {}); - return; - } const yoga = createYoga({ logging: false, maskedErrors: false, @@ -36,7 +34,7 @@ describe('Apollo Link', () => { time: { async *subscribe() { while (true) { - await new Promise(resolve => setTimeout(resolve, 1000)); + await setTimeout(300); yield new Date().toISOString(); } }, @@ -58,6 +56,13 @@ describe('Apollo Link', () => { cache: new InMemoryCache(), }); + beforeEach(() => {}); + + afterAll(() => { + client.stop(); + return client.clearStore(); + }); + it('should handle queries correctly', async () => { const result = await client.query({ query: parse(/* GraphQL */ ` @@ -72,7 +77,7 @@ describe('Apollo Link', () => { hello: 'Hello Apollo Client!', }); }); - it('should handle subscriptions correctly', async () => { + testIf(!process.env['LEAK_TEST'])('should handle subscriptions correctly', async () => { expect.assertions(5); const observable = client.subscribe({ query: parse(/* GraphQL */ ` @@ -83,7 +88,7 @@ describe('Apollo Link', () => { }); const collectedValues: string[] = []; let i = 0; - await new Promise(resolve => { + await new Promise((resolve, reject) => { const subscription = observable.subscribe((result: FetchResult) => { collectedValues.push(result.data?.['time']); i++; @@ -91,7 +96,7 @@ describe('Apollo Link', () => { subscription.unsubscribe(); resolve(); } - }); + }, reject); }); expect(collectedValues.length).toBe(3); expect(i).toBe(3); diff --git a/packages/executors/urql-exchange/src/index.ts b/packages/executors/urql-exchange/src/index.ts index f6f50f003cd..ae0ee4fa3fe 100644 --- a/packages/executors/urql-exchange/src/index.ts +++ b/packages/executors/urql-exchange/src/index.ts @@ -1,6 +1,6 @@ import { OperationTypeNode } from 'graphql'; import { filter, make, merge, mergeMap, pipe, share, Source, takeUntil } from 'wonka'; -import { ExecutionRequest, Executor, isAsyncIterable } from '@graphql-tools/utils'; +import { ExecutionRequest, Executor, fakePromise, isAsyncIterable } from '@graphql-tools/utils'; import { AnyVariables, Exchange, @@ -37,31 +37,35 @@ export function executorExchange(executor: Executor): Exchange { }; return make>(observer => { let ended = false; - Promise.resolve(executor(executionRequest)) - .then(async result => { + fakePromise() + .then(() => executor(executionRequest)) + .then(result => { if (ended || !result) { return; } if (!isAsyncIterable(result)) { observer.next(makeResult(operation, result as ExecutionResult)); + observer.complete(); } else { let prevResult: OperationResult | null = null; - for await (const value of result) { - if (value) { - if (prevResult && value.incremental) { - prevResult = mergeResultPatch(prevResult, value as ExecutionResult); - } else { - prevResult = makeResult(operation, value as ExecutionResult); + return fakePromise().then(async () => { + for await (const value of result) { + if (value) { + if (prevResult && value.incremental) { + prevResult = mergeResultPatch(prevResult, value as ExecutionResult); + } else { + prevResult = makeResult(operation, value as ExecutionResult); + } + observer.next(prevResult); + } + if (ended) { + break; } - observer.next(prevResult); - } - if (ended) { - break; } - } + observer.complete(); + }); } - observer.complete(); }) .catch(error => { observer.next(makeErrorResult(operation, error)); diff --git a/packages/executors/urql-exchange/tests/browser-urql-exchange.spec.ts b/packages/executors/urql-exchange/tests/urql-exchange.spec.ts similarity index 91% rename from packages/executors/urql-exchange/tests/browser-urql-exchange.spec.ts rename to packages/executors/urql-exchange/tests/urql-exchange.spec.ts index b00c42443a6..2b10adb003f 100644 --- a/packages/executors/urql-exchange/tests/browser-urql-exchange.spec.ts +++ b/packages/executors/urql-exchange/tests/urql-exchange.spec.ts @@ -1,16 +1,13 @@ +import { setTimeout } from 'timers/promises'; import { createSchema, createYoga } from 'graphql-yoga'; import { pipe, toObservable } from 'wonka'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; import { ExecutionResult } from '@graphql-tools/utils'; import { createClient } from '@urql/core'; +import { testIf } from '../../../testing/utils.js'; import { executorExchange } from '../src/index.js'; describe('URQL Yoga Exchange', () => { - if (!process.env['TEST_BROWSER']) { - it('skips', () => {}); - return; - } - const aCharCode = 'a'.charCodeAt(0); const yoga = createYoga({ logging: false, maskedErrors: false, @@ -39,8 +36,9 @@ describe('URQL Yoga Exchange', () => { async *subscribe() { let i = 0; while (true) { + const aCharCode = 'a'.charCodeAt(0); yield String.fromCharCode(aCharCode + i); - await new Promise(resolve => setTimeout(resolve, 300)); + await setTimeout(300); i++; } }, @@ -80,7 +78,7 @@ describe('URQL Yoga Exchange', () => { hello: 'Hello Urql Client!', }); }); - it('should handle subscriptions correctly', async () => { + testIf(!process.env['LEAK_TEST'])('should handle subscriptions correctly', async () => { const observable = pipe( client.subscription( /* GraphQL */ ` diff --git a/packages/import/tests/schema/import-schema.spec.ts b/packages/import/tests/schema/import-schema.spec.ts index b7009c58bcf..524f4566e7d 100644 --- a/packages/import/tests/schema/import-schema.spec.ts +++ b/packages/import/tests/schema/import-schema.spec.ts @@ -422,21 +422,21 @@ describe('importSchema', () => { expect(importSchema('./fixtures/directive/c.graphql')).toBeSimilarGqlDoc(expectedSDL); }); - // TODO: later - test.skip('importSchema: multiple key directive', () => { - const expectedSDL = /* GraphQL */ ` - scalar UPC - - scalar SKU - - type Product @key(fields: "upc") @key(fields: "sku") { - upc: UPC! - sku: SKU! - name: String - } - `; - expect(importSchema('./fixtures/directive/e.graphql')).toBeSimilarGqlDoc(expectedSDL); - }); + // // TODO: later + // test.skip('importSchema: multiple key directive', () => { + // const expectedSDL = /* GraphQL */ ` + // scalar UPC + + // scalar SKU + + // type Product @key(fields: "upc") @key(fields: "sku") { + // upc: UPC! + // sku: SKU! + // name: String + // } + // `; + // expect(importSchema('./fixtures/directive/e.graphql')).toBeSimilarGqlDoc(expectedSDL); + // }); test('importSchema: external directive', () => { const expectedSDL = /* GraphQL */ ` diff --git a/packages/loaders/url/.gitignore b/packages/loaders/url/.gitignore deleted file mode 100644 index 043f8173475..00000000000 --- a/packages/loaders/url/.gitignore +++ /dev/null @@ -1 +0,0 @@ -tests/webpack.js diff --git a/packages/loaders/url/tests/url-loader-browser.spec.ts b/packages/loaders/url/tests/url-loader-browser.spec.ts index 0433c46b7a8..ff3f517a57e 100644 --- a/packages/loaders/url/tests/url-loader-browser.spec.ts +++ b/packages/loaders/url/tests/url-loader-browser.spec.ts @@ -1,115 +1,135 @@ import fs from 'fs'; import http from 'http'; -import path from 'path'; +import { AddressInfo, Socket } from 'net'; +import { platform, tmpdir } from 'os'; +import path, { join } from 'path'; +import { setTimeout } from 'timers/promises'; import { parse } from 'graphql'; -import { createSchema, createYoga } from 'graphql-yoga'; +import { createSchema, createYoga, Repeater } from 'graphql-yoga'; import puppeteer, { Browser, Page } from 'puppeteer'; import webpack, { Stats } from 'webpack'; import { useEngine } from '@envelop/core'; import { normalizedExecutor } from '@graphql-tools/executor'; -import { ExecutionResult } from '@graphql-tools/utils'; +import { createDeferred, ExecutionResult } from '@graphql-tools/utils'; import { useDeferStream } from '@graphql-yoga/plugin-defer-stream'; +import { describeIf } from '../../../testing/utils.js'; import type * as UrlLoaderModule from '../src/index.js'; -import { sleep } from './test-utils.js'; - -describe('[url-loader] webpack bundle compat', () => { - if (process.env['TEST_BROWSER']) { - let httpServer: http.Server; - let browser: Browser; - let page: Page; - let resolveOnReturn: VoidFunction; - const timeouts = new Set(); - const fakeAsyncIterable = { - [Symbol.asyncIterator]() { - return this; - }, - next: () => - sleep(300, timeout => timeouts.add(timeout)).then(() => ({ value: true, done: false })), - return: () => { - resolveOnReturn(); - timeouts.forEach(clearTimeout); - return Promise.resolve({ done: true }); - }, - }; - const port = 8712; - const httpAddress = 'http://localhost:8712'; - const webpackBundlePath = path.resolve(__dirname, 'webpack.js'); - const yoga = createYoga({ - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - foo: Boolean - countdown(from: Int): [Int] - fakeStream: [Boolean] - } - type Subscription { - foo: Boolean - } - `, - resolvers: { - Query: { - foo: () => new Promise(resolve => setTimeout(() => resolve(true), 300)), - countdown: async function* (_, { from }) { - for (let i = from; i >= 0; i--) { - yield i; - await new Promise(resolve => setTimeout(resolve, 100)); - } - }, - fakeStream: () => fakeAsyncIterable, + +declare global { + interface Window { + GraphQLToolsUrlLoader: typeof UrlLoaderModule; + } +} + +describeIf(platform() !== 'win32')('[url-loader] webpack bundle compat', () => { + let httpServer: http.Server; + let browser: Browser; + let page: Page; + const fakeAsyncIterableReturnDeferred = createDeferred(); + const yoga = createYoga({ + schema: createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + foo: Boolean + countdown(from: Int): [Int] + fakeStream: [Boolean] + } + type Subscription { + foo: Boolean + } + `, + resolvers: { + Query: { + foo: () => setTimeout(300).then(() => true), + countdown: async function* (_, { from }) { + for (let i = from; i >= 0; i--) { + yield i; + await setTimeout(100); + } }, - Subscription: { - foo: { - async *subscribe() { - await new Promise(resolve => setTimeout(resolve, 300)); - yield { foo: true }; - await new Promise(resolve => setTimeout(resolve, 300)); - yield { foo: false }; - }, + fakeStream: () => + new Repeater(function (push, stop) { + let timeout: ReturnType; + function tick() { + push(true).finally(() => { + timeout = globalThis.setTimeout(tick, 300); + }); + } + tick(); + stop.finally(() => { + if (timeout) { + clearTimeout(timeout); + } + fakeAsyncIterableReturnDeferred.resolve(); + }); + }), + }, + Subscription: { + foo: { + async *subscribe() { + await setTimeout(300); + yield { foo: true }; + await setTimeout(300); + yield { foo: false }; }, }, }, + }, + }), + plugins: [ + useEngine({ + execute: normalizedExecutor, + subscribe: normalizedExecutor, }), - plugins: [ - useEngine({ - execute: normalizedExecutor, - subscribe: normalizedExecutor, - }), - useDeferStream(), - ], - }); - - beforeAll(async () => { - // bundle webpack js - const stats = await new Promise((resolve, reject) => { - webpack( - { - mode: 'development', - entry: path.resolve(__dirname, '..', 'dist', 'esm', 'index.js'), - output: { - path: path.resolve(__dirname), - filename: 'webpack.js', - libraryTarget: 'umd', - library: 'GraphQLToolsUrlLoader', - umdNamedDefine: true, - }, - plugins: [ - new webpack.DefinePlugin({ - setImmediate: 'setTimeout', - }), - ], + useDeferStream(), + ], + }); + + let httpAddress: string; + + const sockets = new Set(); + + const webpackBundleFileName = 'webpack.js'; + const webpackBundleDir = join(tmpdir(), 'graphql-tools-url-loader'); + const webpackBundleFullPath = path.resolve(webpackBundleDir, webpackBundleFileName); + + beforeAll(async () => { + // bundle webpack js + const stats = await new Promise((resolve, reject) => { + const compiler = webpack( + { + mode: 'development', + entry: path.resolve(__dirname, '..', 'dist', 'esm', 'index.js'), + output: { + path: webpackBundleDir, + filename: webpackBundleFileName, + libraryTarget: 'umd', + library: 'GraphQLToolsUrlLoader', + umdNamedDefine: true, }, - (err, stats) => { - if (err) return reject(err); - resolve(stats); - }, - ); - }); + }, + (err, stats) => { + if (err) { + reject(err); + } else { + compiler.close(err => { + if (err) { + reject(err); + } else { + resolve(stats); + } + }); + } + }, + ); + }); - if (stats?.hasErrors()) { - console.error(stats.toString({ colors: true })); - } + if (stats?.hasErrors()) { + throw stats.toString({ colors: true }); + } - httpServer = http.createServer((req, res) => { + httpServer = http + .createServer((req, res) => { if (req.method === 'GET' && req.url === '/') { res.statusCode = 200; res.writeHead(200, { @@ -120,7 +140,7 @@ describe('[url-loader] webpack bundle compat', () => { Url Loader Test - + `); @@ -128,258 +148,293 @@ describe('[url-loader] webpack bundle compat', () => { return; } - if (req.method === 'GET' && req.url === '/webpack.js') { - const stat = fs.statSync(webpackBundlePath); + if (req.method === 'GET' && req.url === '/' + webpackBundleFileName) { + const stat = fs.statSync(webpackBundleFullPath); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Content-Length': stat.size, }); - const readStream = fs.createReadStream(webpackBundlePath); + const readStream = fs.createReadStream(webpackBundleFullPath); readStream.pipe(res); return; } yoga(req, res); + }) + .on('connection', socket => { + sockets.add(socket); + socket.once('close', () => { + sockets.delete(socket); + }); }); - await new Promise(resolve => { - httpServer.listen(port, () => { - resolve(); - }); + const { port } = await new Promise(resolve => { + httpServer.listen(0, () => { + resolve(httpServer.address() as AddressInfo); }); - browser = await puppeteer.launch({ - // headless: false, - args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + httpAddress = `http://localhost:${port}`; + browser = await puppeteer.launch({ + // headless: false, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--incognito'], + }); + page = await browser.newPage(); + await page.goto(httpAddress); + }); + + afterAll(async () => { + if (page) { + await page.close().catch(e => { + console.warn('Error closing page', e, 'ignoring'); }); - page = await browser.newPage(); - await page.goto(httpAddress); - }, 90_000); - - afterAll(async () => { + } + if (browser) { await browser.close(); - await new Promise((resolve, reject) => { + } + await new Promise((resolve, reject) => { + for (const socket of sockets) { + socket.destroy(); + } + if (httpServer) { + httpServer.closeAllConnections(); httpServer.close(err => { if (err) return reject(err); resolve(); }); - }); - await fs.promises.unlink(webpackBundlePath); + } else { + resolve(); + } }); - - it('can be exposed as a global', async () => { - const result = await page.evaluate(async () => { - return typeof (window as any)['GraphQLToolsUrlLoader']; - }); - expect(result).toEqual('object'); + if (fs.existsSync(webpackBundleFullPath)) { + await fs.promises.unlink(webpackBundleFullPath); + } + }); + + it('can be exposed as a global', async () => { + const result = await page.evaluate(() => { + return typeof window.GraphQLToolsUrlLoader; }); + expect(result).toEqual('object'); + }); - it('can be used for executing a basic http query operation', async () => { - const expectedData = { - data: { - foo: true, - }, - }; - const document = parse(/* GraphQL */ ` - query { + it('can be used for executing a basic http query operation', async () => { + const expectedData = { + data: { + foo: true, + }, + }; + const document = parse(/* GraphQL */ ` + query { + foo + } + `); + + const result = await page.evaluate( + (httpAddress, document) => { + const module = window.GraphQLToolsUrlLoader; + const loader = new module.UrlLoader(); + const executor = loader.getExecutorAsync(httpAddress + '/graphql'); + return executor({ + document, + }); + }, + httpAddress, + document, + ); + expect(result).toStrictEqual(expectedData); + }); + + it('handles executing a @defer operation using multipart responses', async () => { + const document = parse(/* GraphQL */ ` + query { + ... on Query @defer { foo } - `); - - const result = await page.evaluate( - async (httpAddress, document) => { - const module = (window as any)['GraphQLToolsUrlLoader'] as typeof UrlLoaderModule; - const loader = new module.UrlLoader(); - const executor = loader.getExecutorAsync(httpAddress + '/graphql'); - const result = await executor({ - document, - }); - return result; - }, - httpAddress, - document as any, - ); - expect(result).toStrictEqual(expectedData); - }); - - it('handles executing a @defer operation using multipart responses', async () => { - const document = parse(/* GraphQL */ ` - query { - ... on Query @defer { - foo - } + } + `); + + const results = await page.evaluate( + async (httpAddress, document) => { + const module = window.GraphQLToolsUrlLoader; + const loader = new module.UrlLoader(); + const executor = loader.getExecutorAsync(httpAddress + '/graphql'); + const result = await executor({ + document, + }); + if (!(Symbol.asyncIterator in result)) { + throw new Error('Expected an async iterator'); } - `); - - const results = await page.evaluate( - async (httpAddress, document) => { - const module = (window as any)['GraphQLToolsUrlLoader'] as typeof UrlLoaderModule; - const loader = new module.UrlLoader(); - const executor = loader.getExecutorAsync(httpAddress + '/graphql'); - const result = await executor({ - document, - }); - const results: any[] = []; - for await (const currentResult of result as any[]) { - if (currentResult) { - results.push(JSON.parse(JSON.stringify(currentResult))); - } + const results: ExecutionResult[] = []; + for await (const currentResult of result) { + if (currentResult) { + results.push(JSON.parse(JSON.stringify(currentResult))); } - return results; - }, - httpAddress, - document as any, - ); - expect(results).toEqual([{ data: {} }, { data: { foo: true } }]); - }); - - it('handles executing a @stream operation using multipart responses', async () => { - const document = parse(/* GraphQL */ ` - query { - countdown(from: 3) @stream } - `); - - const results = await page.evaluate( - async (httpAddress, document) => { - const module = (window as any)['GraphQLToolsUrlLoader'] as typeof UrlLoaderModule; - const loader = new module.UrlLoader(); - const executor = loader.getExecutorAsync(httpAddress + '/graphql'); - const result = await executor({ - document, - }); - const results: any[] = []; - for await (const currentResult of result as any[]) { - if (currentResult) { - results.push(JSON.parse(JSON.stringify(currentResult))); - } + return results; + }, + httpAddress, + document, + ); + expect(results).toEqual([{ data: {} }, { data: { foo: true } }]); + }); + + it('handles executing a @stream operation using multipart responses', async () => { + const document = parse(/* GraphQL */ ` + query { + countdown(from: 3) @stream + } + `); + + const results = await page.evaluate( + async (httpAddress, document) => { + const module = window.GraphQLToolsUrlLoader; + const loader = new module.UrlLoader(); + const executor = loader.getExecutorAsync(httpAddress + '/graphql'); + const result = await executor({ + document, + }); + if (!(Symbol.asyncIterator in result)) { + throw new Error('Expected an async iterator'); + } + const results: ExecutionResult[] = []; + for await (const currentResult of result) { + if (currentResult) { + results.push(JSON.parse(JSON.stringify(currentResult))); } - return results; - }, - httpAddress, - document as any, - ); - - expect(results[0]).toEqual({ data: { countdown: [] } }); - expect(results[1]).toEqual({ data: { countdown: [3] } }); - expect(results[2]).toEqual({ data: { countdown: [3, 2] } }); - expect(results[3]).toEqual({ data: { countdown: [3, 2, 1] } }); - expect(results[4]).toEqual({ data: { countdown: [3, 2, 1, 0] } }); - }); + } + return results; + }, + httpAddress, + document, + ); + + expect(results[0]).toEqual({ data: { countdown: [] } }); + expect(results[1]).toEqual({ data: { countdown: [3] } }); + expect(results[2]).toEqual({ data: { countdown: [3, 2] } }); + expect(results[3]).toEqual({ data: { countdown: [3, 2, 1] } }); + expect(results[4]).toEqual({ data: { countdown: [3, 2, 1, 0] } }); + }); + + it('handles SSE subscription operations', async () => { + const expectedDatas = [{ data: { foo: true } }, { data: { foo: false } }]; + + const document = parse(/* GraphQL */ ` + subscription { + foo + } + `); + + const result = await page.evaluate( + async (httpAddress, document) => { + const module = window.GraphQLToolsUrlLoader; + const loader = new module.UrlLoader(); + const executor = loader.getExecutorAsync(httpAddress + '/graphql', { + subscriptionsProtocol: module.SubscriptionProtocol.SSE, + }); + const result = await executor({ + document, + }); + if (!(Symbol.asyncIterator in result)) { + throw new Error('Expected an async iterator'); + } + const results: ExecutionResult[] = []; + for await (const currentResult of result) { + results.push(currentResult); + } + return results; + }, + httpAddress, + document, + ); + expect(result).toStrictEqual(expectedDatas); + }); + it('terminates SSE subscriptions when calling return on the AsyncIterator', async () => { + const sentDatas = [{ data: { foo: true } }, { data: { foo: false } }, { data: { foo: true } }]; + + const document = parse(/* GraphQL */ ` + subscription { + foo + } + `); - it('handles SSE subscription operations', async () => { - const expectedDatas = [{ data: { foo: true } }, { data: { foo: false } }]; + const pageerrorFn = jest.fn(); + page.on('pageerror', pageerrorFn); - const document = parse(/* GraphQL */ ` - subscription { - foo + const result = await page.evaluate( + async (httpAddress, document) => { + const module = window.GraphQLToolsUrlLoader; + const loader = new module.UrlLoader(); + const executor = loader.getExecutorAsync(httpAddress + '/graphql', { + subscriptionsProtocol: module.SubscriptionProtocol.SSE, + }); + const result = await executor({ + document, + }); + if (!(Symbol.asyncIterator in result)) { + throw new Error('Expected an async iterator'); } - `); - - const result = await page.evaluate( - async (httpAddress, document) => { - const module = (window as any)['GraphQLToolsUrlLoader'] as typeof UrlLoaderModule; - const loader = new module.UrlLoader(); - const executor = loader.getExecutorAsync(httpAddress + '/graphql', { - subscriptionsProtocol: module.SubscriptionProtocol.SSE, - }); - const result = await executor({ - document, - }); - const results: any[] = []; - for await (const currentResult of result as AsyncIterable) { - results.push(currentResult); + const results: ExecutionResult[] = []; + for await (const currentResult of result) { + results.push(currentResult); + if (results.length === 2) { + break; } - return results; - }, - httpAddress, - document as any, - ); - expect(result).toStrictEqual(expectedDatas); - }); - it('terminates SSE subscriptions when calling return on the AsyncIterator', async () => { - const sentDatas = [ - { data: { foo: true } }, - { data: { foo: false } }, - { data: { foo: true } }, - ]; - - const document = parse(/* GraphQL */ ` - subscription { - foo } - `); - - const pageerrorFn = jest.fn(); - page.on('pageerror', pageerrorFn); - - const result = await page.evaluate( - async (httpAddress, document) => { - const module = (window as any)['GraphQLToolsUrlLoader'] as typeof UrlLoaderModule; - const loader = new module.UrlLoader(); - const executor = loader.getExecutorAsync(httpAddress + '/graphql', { - subscriptionsProtocol: module.SubscriptionProtocol.SSE, - }); - const result = (await executor({ - document, - })) as AsyncIterableIterator; - const results: any[] = []; - for await (const currentResult of result) { - results.push(currentResult); - if (results.length === 2) { - break; - } + return results; + }, + httpAddress, + document, + ); + + expect(result).toStrictEqual(sentDatas.slice(0, 2)); + + // no uncaught errors should be reported (browsers raise errors when canceling requests) + expect(pageerrorFn).not.toBeCalled(); + }); + it('terminates stream correctly', async () => { + const document = parse(/* GraphQL */ ` + query { + fakeStream @stream + } + `); + + const pageerrorFn = jest.fn(); + page.once('pageerror', pageerrorFn); + + const currentResult: ExecutionResult = await page.evaluate( + async (httpAddress, document) => { + const module = window.GraphQLToolsUrlLoader; + const loader = new module.UrlLoader(); + const executor = loader.getExecutorAsync(httpAddress + '/graphql'); + const result = await executor({ + document, + }); + if (!(Symbol.asyncIterator in result)) { + throw new Error('Expected an async iterator'); + } + for await (const currentResult of result) { + if (currentResult?.data?.fakeStream?.length > 1) { + return JSON.parse(JSON.stringify(currentResult)); } - return results; - }, - httpAddress, - document as any, - ); - - expect(result).toStrictEqual(sentDatas.slice(0, 2)); - - // no uncaught errors should be reported (browsers raise errors when canceling requests) - expect(pageerrorFn).not.toBeCalled(); - }); - it('terminates stream correctly', async () => { - const document = parse(/* GraphQL */ ` - query { - fakeStream @stream } - `); + }, + httpAddress, + document, + ); - const pageerrorFn = jest.fn(); - page.on('pageerror', pageerrorFn); + await fakeAsyncIterableReturnDeferred.promise; - const returnPromise$ = new Promise(resolve => { - resolveOnReturn = resolve; - }); + page.off('pageerror', pageerrorFn); - await page.evaluate( - async (httpAddress, document) => { - const module = (window as any)['GraphQLToolsUrlLoader'] as typeof UrlLoaderModule; - const loader = new module.UrlLoader(); - const executor = loader.getExecutorAsync(httpAddress + '/graphql'); - const result = (await executor({ - document, - })) as AsyncIterableIterator; - for await (const currentResult of result) { - if (currentResult?.data?.fakeStream?.length > 1) { - break; - } - } - }, - httpAddress, - document as any, - ); + // no uncaught errors should be reported (browsers raise errors when canceling requests) + expect(pageerrorFn).not.toHaveBeenCalled(); - await returnPromise$; - - // no uncaught errors should be reported (browsers raise errors when canceling requests) - expect(pageerrorFn).not.toBeCalled(); + expect(currentResult).toEqual({ + data: { + fakeStream: [true, true], + }, }); - } else { - it('dummy', () => {}); - } + }); }); diff --git a/packages/loaders/url/tests/yoga-compat.spec.ts b/packages/loaders/url/tests/yoga-compat.spec.ts index 6ba26d36149..0a36cbdd032 100644 --- a/packages/loaders/url/tests/yoga-compat.spec.ts +++ b/packages/loaders/url/tests/yoga-compat.spec.ts @@ -11,16 +11,6 @@ import { InMemoryLiveQueryStore } from '@n1ru4l/in-memory-live-query-store'; import { SubscriptionProtocol, UrlLoader } from '../src'; import { assertAsyncIterable, sleep } from './test-utils'; -if (!globalThis.DOMException) { - // @ts-expect-error DOMException is not defined in NodeJS 16 - globalThis.DOMException = class DOMException extends Error { - constructor(message: string, name: string) { - super(message); - this.name = name; - } - }; -} - describe('Yoga Compatibility', () => { jest.setTimeout(10000); const loader = new UrlLoader(); diff --git a/packages/executor/src/execution/__tests__/simplePubSub.ts b/packages/testing/simplePubSub.ts similarity index 93% rename from packages/executor/src/execution/__tests__/simplePubSub.ts rename to packages/testing/simplePubSub.ts index 9e4a27af8a1..cbc4ebf4913 100644 --- a/packages/executor/src/execution/__tests__/simplePubSub.ts +++ b/packages/testing/simplePubSub.ts @@ -69,8 +69,9 @@ export class SimplePubSub { const value: R = transform(event); if (pullQueue.length > 0) { const receiver = pullQueue.shift(); - expect(receiver != null).toBeTruthy(); - // @ts-expect-error + if (!receiver) { + throw new Error('Invalid state'); + } receiver({ value, done: false }); } else { pushQueue.push(value); @@ -78,7 +79,3 @@ export class SimplePubSub { } } } - -describe.skip('no simplePubSub tests', () => { - it.todo('nothing to test'); -}); diff --git a/packages/testing/utils.ts b/packages/testing/utils.ts index d5accd44239..7296279f0cb 100644 --- a/packages/testing/utils.ts +++ b/packages/testing/utils.ts @@ -83,3 +83,11 @@ function findProjectDir(dirname: string): string | never { throw new Error(`Couldn't find project's root from: ${originalDirname}`); } + +export function describeIf(condition: boolean) { + return condition ? describe : describe.skip; +} + +export function testIf(condition: boolean) { + return condition ? test : test.skip; +} diff --git a/packages/utils/src/fakePromise.ts b/packages/utils/src/fakePromise.ts index 8a406f3de19..3eee4932adc 100644 --- a/packages/utils/src/fakePromise.ts +++ b/packages/utils/src/fakePromise.ts @@ -26,7 +26,10 @@ export function fakeRejectPromise(error: unknown): Promise { }; } -export function fakePromise(value: T): Promise { +export function fakePromise(value: T): Promise; +export function fakePromise(value: T): Promise; +export function fakePromise(value: void): Promise; +export function fakePromise(value: T): Promise { if (isPromise(value)) { return value; } diff --git a/patches/jest-leak-detector+29.7.0.patch b/patches/jest-leak-detector+29.7.0.patch new file mode 100644 index 00000000000..a26217e3fbc --- /dev/null +++ b/patches/jest-leak-detector+29.7.0.patch @@ -0,0 +1,33 @@ +diff --git a/node_modules/jest-leak-detector/build/index.js b/node_modules/jest-leak-detector/build/index.js +index a8ccb1e..70699fd 100644 +--- a/node_modules/jest-leak-detector/build/index.js ++++ b/node_modules/jest-leak-detector/build/index.js +@@ -74,26 +74,14 @@ class LeakDetector { + value = null; + } + async isLeaking() { +- this._runGarbageCollector(); ++ (0, _v().setFlagsFromString)('--allow-natives-syntax'); + + // wait some ticks to allow GC to run properly, see https://github.com/nodejs/node/issues/34636#issuecomment-669366235 + for (let i = 0; i < 10; i++) { ++ eval('%CollectGarbage(true)'); + await tick(); + } + return this._isReferenceBeingHeld; + } +- _runGarbageCollector() { +- // @ts-expect-error: not a function on `globalThis` +- const isGarbageCollectorHidden = globalThis.gc == null; +- +- // GC is usually hidden, so we have to expose it before running. +- (0, _v().setFlagsFromString)('--expose-gc'); +- (0, _vm().runInNewContext)('gc')(); +- +- // The GC was not initially exposed, so let's hide it again. +- if (isGarbageCollectorHidden) { +- (0, _v().setFlagsFromString)('--no-expose-gc'); +- } +- } + } + exports.default = LeakDetector; diff --git a/scripts/postbuild.ts b/scripts/postbuild.ts index 6b957b332ec..5786ab9486b 100644 --- a/scripts/postbuild.ts +++ b/scripts/postbuild.ts @@ -25,4 +25,6 @@ ${content}`.trimStart(), console.timeEnd('done'); } -main(); +main().catch(e => { + console.warn(`Failed to modify ${filePath}`); +});