diff --git a/.changeset/cool-wolves-own.md b/.changeset/cool-wolves-own.md new file mode 100644 index 000000000..6bea61c07 --- /dev/null +++ b/.changeset/cool-wolves-own.md @@ -0,0 +1,5 @@ +--- +'gqty': minor +--- + +feat(pacakge/gqty): support GraphQL operation names diff --git a/internal/gqless.sketch b/internal/gqless.sketch deleted file mode 100644 index 07f5a49b9..000000000 Binary files a/internal/gqless.sketch and /dev/null differ diff --git a/internal/website/docs/client/fetching-data.mdx b/internal/website/docs/client/fetching-data.mdx index 63c98dfac..7a632cffa 100644 --- a/internal/website/docs/client/fetching-data.mdx +++ b/internal/website/docs/client/fetching-data.mdx @@ -3,7 +3,7 @@ id: fetching-data title: Fetching data --- -#### Prerequisites +### Prerequisites Make sure you've completed [Getting Started](/docs/getting-started) first. @@ -60,7 +60,66 @@ if (possibleData instanceof Promise) { } ``` -## track +## Refetching data + +### refetch + +A special function that accepts object proxies, or functions. + +When dealing with object proxies, it recovers **all** the history of the specific object down the tree of selections, +and refetchs them, returning the same object back after all the resolutions are done. + +On the other hand, when used with a **function**, it calls the function (using the core scheduler) ignoring possible nulls in the middle in the first pass, +and returns whatever the function gave back as a promise. + +```ts +import { query, refetch, resolved } from '../gqty'; + +// ... + +const data = await resolved(() => { + const user = query.user; + + if (user) { + return { + id: user.name, + name: user.name, + }; + } + return null; +}); + +// Later... + +// It will refetch 'id' & 'name' of 'query.user' +const refetchedUser = await refetch(query.user); +``` + +## Advanced Usages + +### Operation name + +A custom operation name can be added to GraphQL queries, mutations and subscriptions. + +This is useful when mocking for unit tests, and advanced server logics. + +```ts +import { query, resolved, inlineResolved } from '../gqty'; + +resolved(() => query.helloWorld, { operationName: 'MyQuery' }); + +inlineResolved(() => query.helloWorld, { operationName: 'MyQuery' }); +``` + +Both `resolved` and `inlineResolved` above will generate the following query: + +```graphql +query MyQuery { + helloWorld +} +``` + +### track The function `track` accepts a callback that gets automatically called every time related data arrives or related cache changes, particularly useful for subscriptions, but it also works for queries. @@ -80,7 +139,7 @@ setTimeout(() => { }, 5000); ``` -### track options +#### track options ```ts track( @@ -103,42 +162,7 @@ track( ); ``` -## Refetching data - -### refetch - -A special function that accepts object proxies, or functions. - -When dealing with object proxies, it recovers **all** the history of the specific object down the tree of selections, -and refetchs them, returning the same object back after all the resolutions are done. - -On the other hand, when used with a **function**, it calls the function (using the core scheduler) ignoring possible nulls in the middle in the first pass, -and returns whatever the function gave back as a promise. - -```ts -import { query, refetch, resolved } from '../gqty'; - -// ... - -const data = await resolved(() => { - const user = query.user; - - if (user) { - return { - id: user.name, - name: user.name, - }; - } - return null; -}); - -// Later... - -// It will refetch 'id' & 'name' of 'query.user' -const refetchedUser = await refetch(query.user); -``` - -## Using the Scheduler directly +### The scheduler GQty exposes the Scheduler API, which is used by the helper functions above. @@ -155,7 +179,7 @@ async function Example() { } ``` -### Error handling +#### Error handling The scheduler resolves to a promise of a `GQtyError`: diff --git a/packages/gqty/src/Accessor/index.ts b/packages/gqty/src/Accessor/index.ts index 94b563084..4fe2bb55e 100644 --- a/packages/gqty/src/Accessor/index.ts +++ b/packages/gqty/src/Accessor/index.ts @@ -13,8 +13,8 @@ import { decycle, isInteger, isObject, - retrocycle, isObjectWithType, + retrocycle, } from '../Utils'; const ProxySymbol = Symbol('gqty-proxy'); @@ -97,9 +97,9 @@ export interface AssignSelections { export interface AccessorCreators< GeneratedSchema extends { - query: {}; - mutation: {}; - subscription: {}; + query: object; + mutation: object; + subscription: object; } > { createAccessor: ( @@ -123,9 +123,9 @@ export interface AccessorCreators< export function createAccessorCreators< GeneratedSchema extends { - query: {}; - mutation: {}; - subscription: {}; + query: object; + mutation: object; + subscription: object; } = never >(innerState: InnerClientState): AccessorCreators { const { diff --git a/packages/gqty/src/Client/client.ts b/packages/gqty/src/Client/client.ts index 80ff9a6f1..67ba5b1aa 100644 --- a/packages/gqty/src/Client/client.ts +++ b/packages/gqty/src/Client/client.ts @@ -21,6 +21,7 @@ import { EventHandler } from '../Events'; import { createPrefetch, Prefetch } from '../Helpers/prefetch'; import { createRefetch, Refetch } from '../Helpers/refetch'; import { createSSRHelpers, HydrateCache, PrepareRender } from '../Helpers/ssr'; +import { createTracker, Track } from '../Helpers/track'; import { createInterceptorManager, InterceptorManager } from '../Interceptor'; import { createNormalizationHandler, @@ -42,7 +43,6 @@ import { Resolvers, RetryOptions, } from './resolvers'; -import { createTracker, Track } from '../Helpers/track'; export interface InnerClientState { allowCache: boolean; @@ -186,9 +186,9 @@ export interface Mutate< export interface GQtyClient< GeneratedSchema extends { - query: {}; - mutation: {}; - subscription: {}; + query: object; + mutation: object; + subscription: object; } > extends PersistenceHelpers { query: GeneratedSchema['query']; diff --git a/packages/gqty/src/Client/resolvers.ts b/packages/gqty/src/Client/resolvers.ts index 5079c5a29..a76f0e2d8 100644 --- a/packages/gqty/src/Client/resolvers.ts +++ b/packages/gqty/src/Client/resolvers.ts @@ -18,13 +18,13 @@ import { import type { FetchEventData } from '../Events'; import type { NormalizationHandler } from '../Normalization'; import type { SchedulerPromiseValue } from '../Scheduler'; -import type { Selection } from '../Selection/selection'; +import type { FetchOptions } from '../Schema/types'; +import { Selection, SelectionType } from '../Selection/selection'; import type { InnerClientState, SubscribeEvents, SubscriptionsClient, } from './client'; -import type { FetchOptions } from '../Schema/types'; export interface ResolveOptions { /** @@ -96,6 +96,11 @@ export interface ResolveOptions { * Pass any extra fetch options */ fetchOptions?: FetchOptions; + + /** + * Query operation name + */ + operationName?: string; } export type RetryOptions = @@ -188,7 +193,7 @@ export interface BuildAndFetchSelections { type: 'query' | 'mutation', cache?: CacheInstance, options?: FetchResolveOptions, - lastTryParam?: boolean | undefined + lastTry?: boolean | undefined ): Promise; } @@ -224,6 +229,10 @@ export interface InlineResolveOptions { * On valid cache data found callback */ onCacheData?: (data: TData) => void; + /** + * Query operation name + */ + operationName?: string; } export function createResolvers( @@ -252,6 +261,7 @@ export function createResolvers( onEmptyResolve, onSelection, onCacheData, + operationName, }: InlineResolveOptions = {} ) { const prevFoundValidCache = innerState.foundValidCache; @@ -260,7 +270,8 @@ export function createResolvers( const interceptor = interceptorManager.createInterceptor(); let noSelection = true; - function onScalarSelection() { + function onScalarSelection(selection: Selection) { + selection.operationName = operationName; noSelection = false; } interceptor.selectionAddListeners.add(onScalarSelection); @@ -280,9 +291,9 @@ export function createResolvers( const foundValidCache = innerState.foundValidCache; + innerState.allowCache = prevAllowCache; innerState.foundValidCache = prevFoundValidCache; interceptorManager.removeInterceptor(interceptor); - innerState.allowCache = prevAllowCache; if (noSelection) { if (process.env.NODE_ENV !== 'production') { @@ -322,9 +333,9 @@ export function createResolvers( return data; } finally { + innerState.allowCache = prevAllowCache; innerState.foundValidCache = prevFoundValidCache; interceptorManager.removeInterceptor(interceptor); - innerState.allowCache = prevAllowCache; } }; @@ -346,6 +357,7 @@ export function createResolvers( onNoCacheFound, onEmptyResolve, fetchOptions, + operationName, }: ResolveOptions = {} ): Promise { const prevFoundValidCache = innerState.foundValidCache; @@ -357,11 +369,11 @@ export function createResolvers( } let tempCache: typeof innerState.clientCache | undefined; - let tempSelectionManager: SelectionManager | undefined; if (noCache) { innerState.clientCache = tempCache = createCache(); } + let tempSelectionManager: SelectionManager | undefined; if (nonSerializableVariables) { innerState.selectionManager = tempSelectionManager = createSelectionManager(); @@ -374,7 +386,8 @@ export function createResolvers( let noSelection = true; - function onScalarSelection() { + function onScalarSelection(selection: Selection) { + selection.operationName = operationName; noSelection = false; } interceptor.selectionAddListeners.add(onScalarSelection); @@ -400,13 +413,11 @@ export function createResolvers( interceptorManager.removeInterceptor(interceptor); if (innerState.foundValidCache) { - if (onCacheData) { - const shouldContinue = onCacheData(data); - - if (!shouldContinue) return data; + if (onCacheData && !onCacheData(data)) { + return data; } - } else if (onNoCacheFound) { - onNoCacheFound(); + } else { + onNoCacheFound?.(); } innerState.foundValidCache = prevFoundValidCache; @@ -521,16 +532,12 @@ export function createResolvers( type: 'query' | 'mutation', cache: CacheInstance = innerState.clientCache, options: FetchResolveOptions = {}, - lastTryParam?: boolean + lastTry?: boolean ): Promise { if (!selections) return; const isLastTry = - lastTryParam === undefined - ? options.retry - ? false - : true - : lastTryParam; + lastTry === undefined ? (options.retry ? false : true) : lastTry; const { query, variables, cachedData, cacheKey } = buildQueryAndCheckTempCache( @@ -548,9 +555,7 @@ export function createResolvers( if (promise) return promise; - promise = LazyPromise(() => { - return resolve(); - }); + promise = LazyPromise(resolve); resolveQueryPromisesMap[cacheKey] = promise; try { @@ -915,29 +920,42 @@ export function createResolvers( cache: CacheInstance = innerState.clientCache, options: FetchResolveOptions = {} ) { - const { querySelections, mutationSelections, subscriptionSelections } = - separateSelectionTypes(selections); + const selectionBranches = separateSelectionTypes(selections); try { - await Promise.all([ - buildAndFetchSelections( - querySelections, - 'query', - cache, - options - ), - buildAndFetchSelections( - mutationSelections, - 'mutation', - cache, - options - ), - buildAndSubscribeSelections( - subscriptionSelections, - cache, - options - ), - ]); + await Promise.all( + selectionBranches.map((selections) => { + const [ + { + noIndexSelections: [{ type }], + }, + ] = selections; + + if (type === SelectionType.Query) { + return buildAndFetchSelections( + selections, + 'query', + cache, + options + ); + } else if (type === SelectionType.Mutation) { + return buildAndFetchSelections( + selections, + 'mutation', + cache, + options + ); + } else if (type === SelectionType.Subscription) { + return buildAndSubscribeSelections( + selections, + cache, + options + ); + } else { + throw new TypeError('Invalid selection type'); + } + }) + ); } catch (err) { throw GQtyError.create(err); } diff --git a/packages/gqty/src/Interceptor/index.ts b/packages/gqty/src/Interceptor/index.ts index 65d7e4fbe..995d4d4c1 100644 --- a/packages/gqty/src/Interceptor/index.ts +++ b/packages/gqty/src/Interceptor/index.ts @@ -8,28 +8,28 @@ export class Interceptor { selectionCacheRefetchListeners = new Set<(selection: Selection) => void>(); addSelection(selection: Selection) { - if (this.listening) { - this.fetchSelections.add(selection); + if (!this.listening) return; - for (const listener of this.selectionAddListeners) { - listener(selection); - } + this.fetchSelections.add(selection); + + for (const listener of this.selectionAddListeners) { + listener(selection); } } addSelectionCache(selection: Selection) { - if (this.listening) { - for (const listener of this.selectionCacheListeners) { - listener(selection); - } + if (!this.listening) return; + + for (const listener of this.selectionCacheListeners) { + listener(selection); } } addSelectionCacheRefetch(selection: Selection) { - if (this.listening && this.selectionCacheRefetchListeners.size) { - for (const listener of this.selectionCacheRefetchListeners) { - listener(selection); - } + if (!this.listening) return; + + for (const listener of this.selectionCacheRefetchListeners) { + listener(selection); } } diff --git a/packages/gqty/src/QueryBuilder/buildQuery.ts b/packages/gqty/src/QueryBuilder/buildQuery.ts index 0adfa77d0..cb4ba720f 100644 --- a/packages/gqty/src/QueryBuilder/buildQuery.ts +++ b/packages/gqty/src/QueryBuilder/buildQuery.ts @@ -47,6 +47,7 @@ export function createQueryBuilder() { const variablesMap = new Map(); const variableTypes: Record = {}; const variablesMapKeyValue: Record = {}; + const [{ operationName }] = selections; if (normalization) { const selectionsSet = new Set(); @@ -64,7 +65,7 @@ export function createQueryBuilder() { } let builtQuery: BuiltQuery | undefined; - let idAcum = ''; + let idAcum = operationName ?? ''; if (isGlobalCache) { for (const { id } of selections) idAcum += id; @@ -73,6 +74,12 @@ export function createQueryBuilder() { } for (const { noIndexSelections } of selections) { + if (noIndexSelections[0]?.key !== type) { + throw new Error( + `Expected root selection of type "${type}", found "${noIndexSelections[0].key}".` + ); + } + const selectionBranches: string[][] = []; function createSelectionBranch( @@ -190,6 +197,10 @@ export function createQueryBuilder() { ); } + if (operationName) { + query = query.replace(type, type + ' ' + operationName); + } + builtQuery = { query, variables, diff --git a/packages/gqty/src/Selection/SelectionManager.ts b/packages/gqty/src/Selection/SelectionManager.ts index 5f82d7907..0ad2cb1d8 100644 --- a/packages/gqty/src/Selection/SelectionManager.ts +++ b/packages/gqty/src/Selection/SelectionManager.ts @@ -1,58 +1,35 @@ import { sha1 } from '@gqty/utils/sha1'; import { serializeVariables } from '../Utils/cachedJSON'; -import { - Selection, - SelectionConstructorArgs, - SelectionType, -} from './selection'; +import { Selection, SelectionConstructorArgs } from './selection'; export function separateSelectionTypes( selections: Selection[] | Set ) { - let querySelections: Selection[] | undefined; - let mutationSelections: Selection[] | undefined; - let subscriptionSelections: Selection[] | undefined; + /** Group selections by root type and operation names. */ + const selectionBranches = new Map(); for (const selection of selections) { - switch (selection.type) { - case SelectionType.Query: { - querySelections ||= []; - querySelections.push(selection); - break; - } - case SelectionType.Mutation: { - mutationSelections ||= []; - mutationSelections.push(selection); - break; - } - case SelectionType.Subscription: { - subscriptionSelections ||= []; - subscriptionSelections.push(selection); - break; - } + const { type, operationName } = selection; + const branchKey = `${type}.${operationName}`; + + if (!selectionBranches.has(branchKey)) { + selectionBranches.set(branchKey, []); } + + selectionBranches.get(branchKey)!.push(selection); } - return { - querySelections, - mutationSelections, - subscriptionSelections, - }; + return [...selectionBranches.values()]; } export interface GetSelection { - ({ - key, - prevSelection, - args, - argTypes, - type, - unions, - }: Pick< - SelectionConstructorArgs, - 'key' | 'prevSelection' | 'args' | 'argTypes' | 'type' | 'unions' - >): Selection; + ( + options: Pick< + SelectionConstructorArgs, + 'key' | 'prevSelection' | 'args' | 'argTypes' | 'type' | 'unions' + > + ): Selection; } export interface SelectionManager { diff --git a/packages/gqty/src/Selection/selection.ts b/packages/gqty/src/Selection/selection.ts index 78124b670..4335b0c61 100644 --- a/packages/gqty/src/Selection/selection.ts +++ b/packages/gqty/src/Selection/selection.ts @@ -9,6 +9,7 @@ export type SelectionConstructorArgs = { key: string | number; prevSelection?: Selection; type?: SelectionType; + operationName?: string; alias?: string; args?: Record; argTypes?: Record; @@ -22,6 +23,8 @@ export class Selection { type: SelectionType; + operationName?: string; + unions?: string[]; args?: Readonly>; @@ -45,34 +48,35 @@ export class Selection { args, argTypes, type, + operationName, alias, unions, id, }: SelectionConstructorArgs) { this.id = id + ''; this.key = key; + this.operationName = operationName; + this.prevSelection = prevSelection ?? null; const pathKey = alias || key; const isInterfaceUnionSelection = key === '$on'; this.cachePath = isInterfaceUnionSelection - ? prevSelection?.cachePath || [] + ? prevSelection?.cachePath ?? [] : prevSelection ? [...prevSelection.cachePath, pathKey] : [pathKey]; this.pathString = isInterfaceUnionSelection - ? prevSelection?.pathString || '' - : prevSelection - ? prevSelection.pathString + '.' + pathKey - : pathKey.toString(); + ? prevSelection?.pathString ?? '' + : `${prevSelection?.pathString.concat('.') ?? ''}${pathKey}`; - const prevSelectionsList = prevSelection?.selectionsList || []; + const prevSelectionsList = prevSelection?.selectionsList ?? []; this.selectionsList = [...prevSelectionsList, this]; - const prevNoSelectionsList = prevSelection?.noIndexSelections || []; + const prevNoSelectionsList = prevSelection?.noIndexSelections ?? []; this.noIndexSelections = typeof key === 'string' @@ -89,9 +93,7 @@ export class Selection { this.argTypes = argTypes; this.unions = unions; - this.type = type || prevSelection?.type || SelectionType.Query; - - if (prevSelection) this.prevSelection = prevSelection; + this.type = type ?? prevSelection?.type ?? SelectionType.Query; } addCofetchSelections(selections: Selection[] | Set) { diff --git a/packages/gqty/src/Utils/debounce.ts b/packages/gqty/src/Utils/debounce.ts index 8c326c8e8..0937df876 100644 --- a/packages/gqty/src/Utils/debounce.ts +++ b/packages/gqty/src/Utils/debounce.ts @@ -2,7 +2,7 @@ export function debounce( fn: (...args: T) => void, delay: number ) { - let timeout: any; + let timeout: NodeJS.Timeout | string | number | undefined = undefined; return function debounced(...args: Parameters) { const execFetch = () => { diff --git a/packages/gqty/test/buildQuery.test.ts b/packages/gqty/test/buildQuery.test.ts index 10446ecd1..8353039cf 100644 --- a/packages/gqty/test/buildQuery.test.ts +++ b/packages/gqty/test/buildQuery.test.ts @@ -8,8 +8,8 @@ import { Selection, SelectionType } from '../src/Selection'; const buildQuery = createQueryBuilder(); -describe('basic', () => { - test('query', () => { +describe('buildQuery()', () => { + it('should builds basic query', () => { const baseSelection = new Selection({ key: 'query', type: SelectionType.Query, @@ -32,8 +32,6 @@ describe('basic', () => { type: 'query', }); - expect(query.trim().startsWith('query{')).toBeTruthy(); - expect(query).toMatchInlineSnapshot(`"query{a b}"`); expect(variables).toBe(undefined); @@ -45,7 +43,7 @@ describe('basic', () => { expect(officialStripIgnoredCharacters(query)).toBe(query); }); - test('deep query with unions', () => { + it('should builds deep query with unions', () => { const baseSelection = new Selection({ key: 'query', type: SelectionType.Query, @@ -100,8 +98,6 @@ describe('basic', () => { type: 'query', }); - expect(query.trim().startsWith('query{')).toBeTruthy(); - expect(query).toMatchInlineSnapshot( `"query{a{b{c{d{...on val1{b a{f}}...on val2{a{f}}}}}}}"` ); @@ -115,7 +111,7 @@ describe('basic', () => { expect(officialStripIgnoredCharacters(query)).toBe(query); }); - test('query args', () => { + it('should queries with arguments', () => { const baseSelection = new Selection({ key: 'query', type: SelectionType.Query, @@ -162,8 +158,6 @@ describe('basic', () => { } ); - expect(query.trim().startsWith('query(')).toBeTruthy(); - expect(query).toMatchInlineSnapshot( `"query($a1:Int!$b2:String!){gqtyAlias_1:a(a:$a1 b:$b2){a_b a_c}d}"` ); @@ -177,7 +171,7 @@ describe('basic', () => { expect(officialStripIgnoredCharacters(query)).toBe(query); }); - test('mutation args', () => { + it('should build mutation with arguments', () => { const baseSelection = new Selection({ key: 'mutation', type: SelectionType.Mutation, @@ -203,8 +197,6 @@ describe('basic', () => { type: 'mutation', }); - expect(query.trim().startsWith('mutation(')).toBeTruthy(); - expect(query).toMatchInlineSnapshot( `"mutation($a1:Int!$b2:String!){gqtyAlias_1:a(a:$a1 b:$b2)}"` ); @@ -217,4 +209,43 @@ describe('basic', () => { expect(officialStripIgnoredCharacters(query)).toBe(query); }); + + it('should fails on mismatched selection type', () => { + const baseSelection = new Selection({ + key: 'mutation', + type: SelectionType.Query, + id: 0, + }); + + expect(() => { + buildQuery([baseSelection], { type: 'query' }); + }).toThrow('Expected root selection of type "query", found "mutation".'); + }); + + it('should query with operation name', () => { + const baseSelection = new Selection({ + key: 'query', + type: SelectionType.Query, + id: 0, + }); + + const selectionA = new Selection({ + key: 'a', + prevSelection: baseSelection, + id: 1, + operationName: 'TestQuery', + }); + + const { query } = buildQuery([selectionA], { + type: 'query', + }); + + expect(query).toMatchInlineSnapshot(`"query TestQuery{a}"`); + + expect(() => { + parse(query); + }).not.toThrow(); + + expect(officialStripIgnoredCharacters(query)).toBe(query); + }); }); diff --git a/packages/gqty/test/client.test.ts b/packages/gqty/test/client.test.ts index 909d07ebe..9491399f1 100644 --- a/packages/gqty/test/client.test.ts +++ b/packages/gqty/test/client.test.ts @@ -87,6 +87,62 @@ describe('core', () => { expect(onCacheData2).toBeCalledTimes(1); }); + + test('resolved with operationName', async () => { + const fetchHistory: string[] = []; + const { query, resolved } = await createTestClient( + undefined, + async (query) => { + fetchHistory.push(query); + return {}; + } + ); + + await Promise.all([ + resolved(() => query.hello, { operationName: 'TestQueryA' }), + resolved(() => query.hello, { operationName: 'TestQueryB' }), + ]); + + expect(fetchHistory).toEqual( + expect.arrayContaining([ + 'query TestQueryA{hello}', + 'query TestQueryB{hello}', + ]) + ); + }); + + test('inlineResolved with operationName', async () => { + const fetchHistory: string[] = []; + const { query, mutation, inlineResolved } = await createTestClient( + undefined, + async (query) => { + fetchHistory.push(query); + await new Promise((resolve) => setTimeout(resolve, 5)); + return { data: { query: 'hello' } }; + } + ); + + await Promise.all([ + inlineResolved(() => query.human({ name: 'John' }).__typename, { + operationName: 'TestQueryA', + }), + inlineResolved( + () => mutation.humanMutation({ nameArg: 'Jane' }).__typename, + { operationName: 'TestMutation' } + ), + inlineResolved(() => query.hello, { + operationName: 'TestQueryB', + }), + ]); + + expect(fetchHistory).toEqual( + expect.arrayContaining([ + 'mutation TestMutation($nameArg1:String!){humanMutation_70c5e_90943:humanMutation(nameArg:$nameArg1){__typename id}}', + 'query TestQueryA($name1:String){human_b8c92_7a4a6:human(name:$name1){__typename id}}', + 'query TestQueryB{hello}', + ]) + ); + }); }); describe('resolved cache options', () => { diff --git a/packages/gqty/test/selection.test.ts b/packages/gqty/test/selection.test.ts index 648889418..cb935e885 100644 --- a/packages/gqty/test/selection.test.ts +++ b/packages/gqty/test/selection.test.ts @@ -104,9 +104,8 @@ describe('selection creation', () => { selectionF, selectionG, ]) - ).toEqual({ - querySelections: [selectionF], - mutationSelections: [ + ).toEqual([ + [ selectionA, selectionB, selectionC, @@ -115,7 +114,8 @@ describe('selection creation', () => { selectionD, repeatSelectionD, ], - subscriptionSelections: [selectionG], - }); + [selectionF], + [selectionG], + ]); }); }); diff --git a/packages/gqty/test/ssr.test.tsx b/packages/gqty/test/ssr.test.tsx index c3fe1f2bd..8a673aec6 100644 --- a/packages/gqty/test/ssr.test.tsx +++ b/packages/gqty/test/ssr.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { renderToString } from 'react-dom/server'; +import semver from 'semver'; import { waitForExpect } from 'test-utils'; import { createTestClient } from './utils'; @@ -111,7 +112,11 @@ describe('server side rendering', () => { .spyOn(console, 'error') .mockImplementation((message) => { expect(message).toEqual( - SyntaxError('Unexpected token i in JSON at position 0') + SyntaxError( + semver.gte(process.version, '18.0.0') + ? `Unexpected token 'i', "invalid" is not valid JSON` + : `Unexpected token i in JSON at position 0` + ) ); }); diff --git a/packages/react/jest.config.js b/packages/react/jest.config.js index d664f2024..9173ce9ef 100644 --- a/packages/react/jest.config.js +++ b/packages/react/jest.config.js @@ -1,6 +1,4 @@ -const { getConfig } = require('test-utils/jest.config.js'); - -module.exports = getConfig({ +module.exports = require('test-utils/jest.config.js').getConfig({ setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], testEnvironment: 'jsdom', }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ebc8e719..2f65fae16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4070,7 +4070,7 @@ packages: '@types/node': 18.11.9 ansi-escapes: 4.3.2 chalk: 4.1.2 - ci-info: 3.5.0 + ci-info: 3.6.1 exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 29.2.0 @@ -4111,7 +4111,7 @@ packages: '@types/node': 18.11.9 ansi-escapes: 4.3.2 chalk: 4.1.2 - ci-info: 3.5.0 + ci-info: 3.6.1 exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 29.2.0 @@ -6535,9 +6535,6 @@ packages: resolution: {integrity: sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==} dev: true - /ci-info/3.5.0: - resolution: {integrity: sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==} - /ci-info/3.6.1: resolution: {integrity: sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==} engines: {node: '>=8'} @@ -9735,7 +9732,7 @@ packages: jest-util: 29.3.1 jest-validate: 29.3.1 prompts: 2.4.2 - yargs: 17.6.0 + yargs: 17.6.2 transitivePeerDependencies: - '@types/node' - supports-color @@ -9762,7 +9759,7 @@ packages: jest-util: 29.3.1 jest-validate: 29.3.1 prompts: 2.4.2 - yargs: 17.6.0 + yargs: 17.6.2 transitivePeerDependencies: - '@types/node' - supports-color @@ -10110,7 +10107,7 @@ packages: '@jest/types': 29.3.1 '@types/node': 18.11.9 chalk: 4.1.2 - ci-info: 3.5.0 + ci-info: 3.6.1 graceful-fs: 4.2.10 picomatch: 2.3.1 @@ -14709,18 +14706,6 @@ packages: yargs-parser: 20.2.9 dev: true - /yargs/17.6.0: - resolution: {integrity: sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==} - engines: {node: '>=12'} - dependencies: - cliui: 8.0.1 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - /yargs/17.6.2: resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==} engines: {node: '>=12'} @@ -14732,7 +14717,6 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - dev: true /yn/3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}