Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/remove-legacy-editor-chrome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'graphiql': minor
---

Remove the duplicated legacy editor toolbar and corner logo. Branding now appears only in the top bar, and the editor actions (prettify, merge fragments, copy, save) live together in the tab strip. Merge Fragments, previously only in the old toolbar, moves into the tab-strip actions.

The action buttons now sit at the right edge of the query editor rather than the far right of the response pane, so they stay next to the query you're editing and follow the editor/response divider as you resize it.
14 changes: 14 additions & 0 deletions packages/graphiql/cypress/e2e/init.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ describe('GraphiQL On Initialization', () => {
'#graphiql',
'.graphiql-container',
'.graphiql-sessions',
'.graphiql-editor-column',
'.graphiql-editors',
'.graphiql-response-column',
'.graphiql-response',
'.graphiql-editor-tool',
];
Expand All @@ -43,6 +45,18 @@ describe('GraphiQL On Initialization', () => {
}
});

it('Places the action buttons on the editor side of the split', () => {
cy.visit('/');
// The prettify/merge/copy/save buttons belong to the query editor, so they
// live in the editor column rather than floating over the response pane.
cy.get('.graphiql-editor-column .graphiql-tab-strip-actions')
.find('.graphiql-tab-strip-action')
.should('have.length.at.least', 3);
cy.get('.graphiql-response-column .graphiql-tab-strip-actions').should(
'not.exist',
);
});

it('Executes a GraphQL query over HTTP that has the expected result', () => {
cy.visitWithOp({ query: testQuery });
cy.clickExecuteQuery();
Expand Down
23 changes: 23 additions & 0 deletions packages/graphiql/cypress/e2e/merge-fragments.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
describe('GraphiQL Merge Fragments', () => {
it('merges a fragment into the operation when clicked', () => {
const query = `fragment IdFragment on Test {
id
}

query TestQuery {
...IdFragment
}`;

cy.visitWithOp({ query });
cy.clickMergeFragments();

cy.get(
'.graphiql-query-editor .view-lines.monaco-mouse-cursor-text',
).should(element => {
const text = element.get(0).innerText;
expect(text).to.not.contain('fragment IdFragment');
expect(text).to.not.contain('...IdFragment');
expect(text).to.contain('id');
});
});
});
10 changes: 8 additions & 2 deletions packages/graphiql/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ declare namespace Cypress {

clickPrettify(): Chainable<Element>;

clickMergeFragments(): Chainable<Element>;

assertHasValues(op: Op): Chainable<Element>;

assertQueryResult(expectedResult: MockResult): Chainable<Element>;
Expand Down Expand Up @@ -141,11 +143,15 @@ Cypress.Commands.add('activateOperation', (operationName: string) => {
});

Cypress.Commands.add('clickExecuteQuery', () => {
cy.get('.graphiql-execute-button').click();
cy.get('[aria-label="Run query"]').click();
});

Cypress.Commands.add('clickPrettify', () => {
cy.get('[aria-label="Prettify query (Shift-Ctrl-P)"]').click();
cy.get('[aria-label="Prettify query"]').click();
});

Cypress.Commands.add('clickMergeFragments', () => {
cy.get('[aria-label="Merge fragments into query"]').click();
});

Cypress.Commands.add('visitWithOp', ({ query, variables, variablesString }) => {
Expand Down
108 changes: 96 additions & 12 deletions packages/graphiql/src/GraphiQL.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,9 @@ describe('GraphiQL', () => {
const { container } = render(<GraphiQL fetcher={noOpFetcher} />);

const dragBar = container.querySelector('.graphiql-horizontal-drag-bar')!;
const editors =
container.querySelector<HTMLDivElement>('.graphiql-editors')!;
const editorColumn = container.querySelector<HTMLDivElement>(
'.graphiql-editor-column',
)!;

act(() => {
fireEvent.mouseDown(dragBar, {
Expand All @@ -374,7 +375,7 @@ describe('GraphiQL', () => {

await waitFor(() => {
// 700 / (900 - 700) = 3.5
expect(editors.style.flexGrow).toEqual('3.5');
expect(editorColumn.style.flexGrow).toEqual('3.5');
});

clientWidthSpy.mockRestore();
Expand Down Expand Up @@ -597,21 +598,23 @@ describe('GraphiQL', () => {
</>
);

const { container, getByRole } = render(
const { container, queryByRole } = render(
<GraphiQL fetcher={noOpFetcher}>{myFragment}</GraphiQL>,
);

await waitFor(() => {
expect(
container.querySelector('.graphiql-container'),
).toBeInTheDocument();
expect(container.querySelector('.graphiql-logo')).toBeInTheDocument();
expect(getByRole('toolbar')).toBeInTheDocument();
expect(
container.querySelector('.graphiql-logo'),
).not.toBeInTheDocument();
expect(queryByRole('toolbar')).not.toBeInTheDocument();
});
});

it('properly ignores non-override children components', async () => {
const { container, getByRole } = render(
const { container, queryByRole } = render(
<GraphiQL fetcher={noOpFetcher}>
<MyFunctionalComponent />
</GraphiQL>,
Expand All @@ -621,8 +624,10 @@ describe('GraphiQL', () => {
expect(
container.querySelector('.graphiql-container'),
).toBeInTheDocument();
expect(container.querySelector('.graphiql-logo')).toBeInTheDocument();
expect(getByRole('toolbar')).toBeInTheDocument();
expect(
container.querySelector('.graphiql-logo'),
).not.toBeInTheDocument();
expect(queryByRole('toolbar')).not.toBeInTheDocument();
});
});

Expand All @@ -634,7 +639,7 @@ describe('GraphiQL', () => {
}
}

const { container, getByRole } = render(
const { container, queryByRole } = render(
<GraphiQL fetcher={noOpFetcher}>
<MyClassComponent />
</GraphiQL>,
Expand All @@ -644,8 +649,10 @@ describe('GraphiQL', () => {
expect(
container.querySelector('.graphiql-container'),
).toBeInTheDocument();
expect(container.querySelector('.graphiql-logo')).toBeInTheDocument();
expect(getByRole('toolbar')).toBeInTheDocument();
expect(
container.querySelector('.graphiql-logo'),
).not.toBeInTheDocument();
expect(queryByRole('toolbar')).not.toBeInTheDocument();
});
});

Expand Down Expand Up @@ -702,6 +709,83 @@ describe('GraphiQL', () => {
});
});

describe('tab strip actions', () => {
it('renders exactly one prettify, merge, copy, and save action, and no legacy toolbar or logo', async () => {
const { container } = render(
<GraphiQL fetcher={noOpFetcher} onSaveQuery={() => {}} />,
);

await waitFor(() => {
expect(
container.querySelectorAll('[aria-label="Prettify query"]'),
).toHaveLength(1);
expect(
container.querySelectorAll(
'[aria-label="Merge fragments into query"]',
),
).toHaveLength(1);
expect(
container.querySelectorAll('[aria-label="Copy query"]'),
).toHaveLength(1);
expect(
container.querySelectorAll('[aria-label="Save query"]'),
).toHaveLength(1);
expect(
container.querySelector('.graphiql-toolbar'),
).not.toBeInTheDocument();
expect(
container.querySelector('.graphiql-logo'),
).not.toBeInTheDocument();
});
});

it('merges fragments into the operation when Merge Fragments is clicked', async () => {
let queryEditor: MonacoEditor;
let documentAST: unknown;

const HookConsumer: FC = () => {
const $queryEditor = useGraphiQL(state => state.queryEditor);
const $documentAST = useGraphiQL(state => state.documentAST);
useEffect(() => {
queryEditor = $queryEditor!;
documentAST = $documentAST;
}, [$queryEditor, $documentAST]);
return null;
};

const { getByLabelText } = render(
<GraphiQL fetcher={noOpFetcher}>
<HookConsumer />
</GraphiQL>,
);

const query = `fragment NameFragment on Query { q }
query TestQuery { ...NameFragment }`;

await waitFor(() => {
expect(queryEditor).toBeTruthy();
});
act(() => {
queryEditor.setValue(query);
});

await waitFor(() => {
expect(queryEditor.getValue()).toContain('...NameFragment');
// The editor debounces content-change handling before it updates the
// document AST that `mergeQuery` reads from.
expect(documentAST).toBeTruthy();
});

fireEvent.click(getByLabelText('Merge fragments into query'));

await waitFor(() => {
const merged = queryEditor.getValue();
expect(merged).not.toContain('...NameFragment');
expect(merged).toContain('q');
});
}, 15_000);
});

it('should support multiple instances', async () => {
const queryEditors: Record<0 | 1, MonacoEditor> = Object.create(null);

Expand Down
Loading
Loading