Skip to content

Commit 416e3a0

Browse files
authored
fix broken useOperationsEditorState and useEditorState hook and add unit tests (#4076)
* upd * upd * upd * upd * upd * Update packages/graphiql/src/GraphiQL.spec.tsx * fix typecheck
1 parent 3a0a755 commit 416e3a0

File tree

6 files changed

+118
-57
lines changed

6 files changed

+118
-57
lines changed

.changeset/early-deers-carry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphiql/react': patch
3+
'graphiql': patch
4+
---
5+
6+
fix broken `useOperationsEditorState` and `useEditorState` hook and add unit tests

packages/graphiql-react/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ export type { TabsState } from './utility/tabs';
77
export * from './icons';
88
export * from './components';
99

10-
export type { EditorProps, SchemaReference, SlicesWithActions } from './types';
10+
export type {
11+
EditorProps,
12+
SchemaReference,
13+
SlicesWithActions,
14+
MonacoEditor,
15+
} from './types';
1116
export type { GraphiQLPlugin } from './stores/plugin';
1217
export { KEY_MAP, formatShortcutForOS, isMacOs } from './constants';
1318
export * from './deprecated';

packages/graphiql-react/src/utility/hooks.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- TODO: check why query builder update only 1st field https://github.com/graphql/graphiql/issues/3836
2-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
32
import { storageStore } from '../stores';
43
import { debounce } from './debounce';
54
import type * as monaco from 'monaco-editor';
@@ -48,23 +47,32 @@ export function useChangeHandler(
4847
export const useEditorState = (
4948
editor: 'query' | 'variable' | 'header',
5049
): [string, (val: string) => void] => {
51-
// eslint-disable-next-line react-hooks/react-compiler -- TODO: check why query builder update only 1st field https://github.com/graphql/graphiql/issues/3836
52-
'use no memo';
5350
const editorInstance = useGraphiQL(state => state[`${editor}Editor`]);
54-
const [editorValue, setEditorValue] = useState(
55-
() => editorInstance?.getValue() ?? '',
56-
);
57-
const handleChange = useCallback(
58-
(value: string) => {
59-
editorInstance?.setValue(value);
60-
setEditorValue(value);
61-
},
62-
[editorInstance],
63-
);
64-
return useMemo(
65-
() => [editorValue, handleChange],
66-
[editorValue, handleChange],
67-
);
51+
const [value, setValue] = useState('');
52+
const model = editorInstance?.getModel();
53+
54+
useEffect(() => {
55+
if (!model) {
56+
return;
57+
}
58+
59+
function onChange() {
60+
setValue(model!.getValue());
61+
}
62+
63+
const disposable = model.onDidChangeContent(onChange);
64+
// Initialize the value
65+
onChange();
66+
return () => {
67+
disposable.dispose();
68+
};
69+
}, [model]);
70+
71+
function handleChange(newValue: string) {
72+
model!.setValue(newValue);
73+
}
74+
75+
return [value, handleChange];
6876
};
6977

7078
/**

packages/graphiql/setup-files.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
'use no memo';
22

33
import '@testing-library/jest-dom';
4+
import { configure } from '@testing-library/react';
45

56
// to make it works like Jest (auto-mocking)
67
vi.mock('zustand');
78
vi.mock('monaco-editor');
9+
10+
// Since we load `monaco-editor` dynamically, we need to allow more time for tests that assert editor values
11+
configure({ asyncUtilTimeout: 8_000 });

packages/graphiql/src/GraphiQL.spec.tsx

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77
* LICENSE file in the root directory of this source tree.
88
*/
99
import { act, render, waitFor, fireEvent } from '@testing-library/react';
10-
import { Component } from 'react';
10+
import { Component, FC, useEffect } from 'react';
1111
import { GraphiQL } from './GraphiQL';
1212
import type { Fetcher } from '@graphiql/toolkit';
13-
import { ToolbarButton } from '@graphiql/react';
13+
import {
14+
ToolbarButton,
15+
useGraphiQL,
16+
useOperationsEditorState,
17+
MonacoEditor,
18+
} from '@graphiql/react';
1419
import '@graphiql/react/setup-workers/vite';
1520

1621
// The smallest possible introspection result that builds a schema.
@@ -155,46 +160,31 @@ describe('GraphiQL', () => {
155160
}); // schema
156161

157162
describe('default query', () => {
158-
const timeout = 8_000;
159-
it(
160-
'defaults to the built-in default query',
161-
async () => {
162-
const { container } = render(<GraphiQL fetcher={noOpFetcher} />);
163-
164-
await waitFor(
165-
() => {
166-
const queryEditor = container.querySelector<HTMLDivElement>(
167-
'.graphiql-editor .monaco-scrollable-element',
168-
);
169-
expect(queryEditor).toBeVisible();
170-
expect(queryEditor!.textContent).toBe('# Welcome to GraphiQL');
171-
},
172-
{ timeout },
173-
);
174-
},
175-
timeout,
176-
);
163+
it('defaults to the built-in default query', async () => {
164+
const { container } = render(<GraphiQL fetcher={noOpFetcher} />);
177165

178-
it(
179-
'accepts a custom default query',
180-
async () => {
181-
const { container } = render(
182-
<GraphiQL fetcher={noOpFetcher} defaultQuery="GraphQL Party!!" />,
166+
await waitFor(() => {
167+
const queryEditor = container.querySelector<HTMLDivElement>(
168+
'.graphiql-editor .monaco-scrollable-element',
183169
);
170+
expect(queryEditor).toBeVisible();
171+
expect(queryEditor!.textContent).toBe('# Welcome to GraphiQL');
172+
});
173+
});
184174

185-
await waitFor(
186-
() => {
187-
const queryEditor = container.querySelector<HTMLDivElement>(
188-
'.graphiql-editor .monaco-scrollable-element',
189-
);
190-
expect(queryEditor).toBeVisible();
191-
expect(queryEditor!.textContent).toBe('GraphQL Party!!');
192-
},
193-
{ timeout },
175+
it('accepts a custom default query', async () => {
176+
const { container } = render(
177+
<GraphiQL fetcher={noOpFetcher} defaultQuery="GraphQL Party!!" />,
178+
);
179+
180+
await waitFor(() => {
181+
const queryEditor = container.querySelector<HTMLDivElement>(
182+
'.graphiql-editor .monaco-scrollable-element',
194183
);
195-
},
196-
timeout,
197-
);
184+
expect(queryEditor).toBeVisible();
185+
expect(queryEditor!.textContent).toBe('GraphQL Party!!');
186+
});
187+
});
198188
}); // default query
199189

200190
// TODO: rewrite these plugin tests after plugin API has more structure
@@ -697,4 +687,50 @@ describe('GraphiQL', () => {
697687
expect(secondEl!.querySelectorAll('.graphiql-tab').length).toBe(1);
698688
});
699689
});
690+
691+
it('`useOperationsEditorState` hook', async () => {
692+
let hookResult: ReturnType<typeof useOperationsEditorState>;
693+
let queryEditor: MonacoEditor;
694+
695+
const HookConsumer: FC = () => {
696+
const $hookResult = useOperationsEditorState();
697+
const $queryEditor = useGraphiQL(state => state.queryEditor);
698+
useEffect(() => {
699+
hookResult = $hookResult;
700+
queryEditor = $queryEditor!;
701+
}, [$hookResult, $queryEditor]);
702+
return null;
703+
};
704+
705+
const { container } = render(
706+
<GraphiQL fetcher={noOpFetcher} initialQuery="query { hello }">
707+
<HookConsumer />
708+
</GraphiQL>,
709+
);
710+
let editor: HTMLDivElement = null!;
711+
712+
// Assert initial values
713+
await waitFor(() => {
714+
editor = container.querySelector<HTMLDivElement>(
715+
'.graphiql-editor .monaco-scrollable-element',
716+
)!;
717+
expect(editor.textContent).toBe('query { hello }');
718+
expect(hookResult[0]).toBe('query { hello }');
719+
});
720+
// Assert value was changed after UI editing
721+
await waitFor(() => {
722+
const newValue = 'bar';
723+
queryEditor.setValue(newValue);
724+
expect(editor.textContent).toBe(newValue);
725+
expect(hookResult[0]).toBe(newValue);
726+
});
727+
728+
// Assert using hook handler
729+
await waitFor(() => {
730+
const newValue = 'foo';
731+
hookResult[1](newValue);
732+
expect(editor.textContent).toBe(newValue);
733+
expect(hookResult[0]).toBe(newValue);
734+
});
735+
});
700736
});

packages/graphiql/vitest.config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export default defineConfig({
88
globals: true,
99
environment: 'jsdom',
1010
setupFiles: ['./setup-files.ts'],
11+
// Since we increased `waitFor` timeout in setup-files.ts
12+
testTimeout: 8_000,
1113
alias: [
1214
{
1315
// Fixes Error: Failed to resolve entry for package "monaco-editor". The package may have incorrect main/module/exports specified in its package.json.

0 commit comments

Comments
 (0)