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
3 changes: 3 additions & 0 deletions packages/graphiql-console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
"vitest": "vitest"
},
"dependencies": {
"@graphiql/toolkit": "^0.11.3",
"@shopify/polaris": "^12.27.0",
"@shopify/polaris-icons": "^9.0.0",
"graphiql": "^3.0.0",
"graphql": "^16.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import {GraphiQLEditor} from './GraphiQLEditor.tsx'
import React from 'react'
import {render} from '@testing-library/react'
import {describe, test, expect, vi, beforeEach} from 'vitest'
import type {GraphiQLConfig} from '@/types/config'

// Mock GraphiQL component
const mockGraphiQL = vi.fn()
vi.mock('graphiql', () => ({
GraphiQL: (props: any) => {
mockGraphiQL(props)
return <div data-testid="graphiql-mock" data-tabs-count={props.defaultTabs?.length || 0} />
},
}))

// Mock createGraphiQLFetcher
const mockCreateFetcher = vi.fn()
vi.mock('@graphiql/toolkit', () => ({
createGraphiQLFetcher: (options: any) => {
mockCreateFetcher(options)
return vi.fn()
},
}))

describe('<GraphiQLEditor />', () => {
const baseConfig: GraphiQLConfig = {
baseUrl: 'http://localhost:3457',
apiVersion: '2024-10',
apiVersions: ['2024-01', '2024-04', '2024-07', '2024-10', 'unstable'],
appName: 'Test App',
appUrl: 'http://localhost:3000',
storeFqdn: 'test-store.myshopify.com',
}

beforeEach(() => {
mockGraphiQL.mockClear()
mockCreateFetcher.mockClear()
})

test('renders GraphiQL component', () => {
render(<GraphiQLEditor config={baseConfig} apiVersion="2024-10" />)

expect(mockGraphiQL).toHaveBeenCalledTimes(1)
})

test('creates fetcher with correct URL including api_version', () => {
render(<GraphiQLEditor config={baseConfig} apiVersion="2024-07" />)

expect(mockCreateFetcher).toHaveBeenCalledWith(
expect.objectContaining({
url: 'http://localhost:3457/graphiql/graphql.json?api_version=2024-07',
}),
)
})

test('creates fetcher without Authorization header when key is not provided', () => {
render(<GraphiQLEditor config={baseConfig} apiVersion="2024-10" />)

expect(mockCreateFetcher).toHaveBeenCalledWith(
expect.objectContaining({
headers: {},
}),
)
})

test('creates fetcher with Authorization header when key is provided', () => {
const configWithKey = {...baseConfig, key: 'test-api-key'}
render(<GraphiQLEditor config={configWithKey} apiVersion="2024-10" />)

expect(mockCreateFetcher).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
Authorization: 'Bearer test-api-key',
},
}),
)
})

test('passes ephemeral storage to GraphiQL', () => {
render(<GraphiQLEditor config={baseConfig} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]
expect(graphiqlCall.storage).toBeDefined()
expect(typeof graphiqlCall.storage.getItem).toBe('function')
expect(typeof graphiqlCall.storage.setItem).toBe('function')
})

test('ephemeral storage returns null for tabs key', () => {
render(<GraphiQLEditor config={baseConfig} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]
const storage = graphiqlCall.storage

expect(storage.getItem('tabs')).toBeNull()
})

test('ephemeral storage does not persist tabs on setItem', () => {
// Mock localStorage
const originalSetItem = Storage.prototype.setItem
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem')

render(<GraphiQLEditor config={baseConfig} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]
const storage = graphiqlCall.storage

storage.setItem('tabs', '[]')
expect(setItemSpy).not.toHaveBeenCalledWith('tabs', expect.anything())

// Other keys should be persisted
storage.setItem('other-key', 'value')
expect(setItemSpy).toHaveBeenCalledWith('other-key', 'value')

setItemSpy.mockRestore()
Storage.prototype.setItem = originalSetItem
})

test('constructs defaultTabs with WELCOME_MESSAGE when no queries provided', () => {
render(<GraphiQLEditor config={baseConfig} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]
const defaultTabs = graphiqlCall.defaultTabs

// Should have WELCOME_MESSAGE + DEFAULT_SHOP_QUERY
expect(defaultTabs).toHaveLength(2)
expect(defaultTabs[0].query).toContain('Welcome to GraphiQL')
expect(defaultTabs[1].query).toContain('query shopInfo')
})

test('includes initial query from config as third tab', () => {
const configWithQuery = {
...baseConfig,
query: 'query test { shop { name } }',
variables: '{"var": "value"}',
}
render(<GraphiQLEditor config={configWithQuery} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]
const defaultTabs = graphiqlCall.defaultTabs

// First tab is WELCOME_MESSAGE, second is DEFAULT_SHOP_QUERY, third is config query
expect(defaultTabs[2].query).toBe('query test { shop { name } }')
expect(defaultTabs[2].variables).toBe('{"var": "value"}')
})

test('always includes DEFAULT_SHOP_QUERY even if config has similar query', () => {
const configWithShopQuery = {
...baseConfig,
query: 'query shopInfo { shop { id name } }',
}
render(<GraphiQLEditor config={configWithShopQuery} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]
const defaultTabs = graphiqlCall.defaultTabs

// Should have: WELCOME_MESSAGE + DEFAULT_SHOP_QUERY + config query (no deduplication)
expect(defaultTabs).toHaveLength(3)
expect(defaultTabs[0].query).toContain('Welcome to GraphiQL')
expect(defaultTabs[1].query).toContain('query shopInfo')
expect(defaultTabs[2].query).toContain('query shopInfo')
})

test('includes defaultQueries from config', () => {
const configWithDefaultQueries = {
...baseConfig,
defaultQueries: [
{query: 'query products { products { edges { node { id } } } }'},
{query: 'query orders { orders { edges { node { id } } } }', variables: '{"first": 10}'},
],
}
render(<GraphiQLEditor config={configWithDefaultQueries} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]
const defaultTabs = graphiqlCall.defaultTabs

// Should have: WELCOME_MESSAGE + DEFAULT_SHOP_QUERY + 2 defaultQueries
expect(defaultTabs).toHaveLength(4)
expect(defaultTabs[2].query).toContain('query products')
expect(defaultTabs[3].query).toContain('query orders')
expect(defaultTabs[3].variables).toBe('{"first": 10}')
})

test('adds preface to defaultQueries when provided', () => {
const configWithPreface = {
...baseConfig,
defaultQueries: [
{
query: 'query test { shop { name } }',
preface: '# This is a test query',
},
],
}
render(<GraphiQLEditor config={configWithPreface} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]
const defaultTabs = graphiqlCall.defaultTabs

expect(defaultTabs[2].query).toBe('# This is a test query\nquery test { shop { name } }')
})

test('WELCOME_MESSAGE is always the first tab', () => {
const configWithMultipleQueries = {
...baseConfig,
query: 'query initial { shop { id } }',
defaultQueries: [
{query: 'query products { products { edges { node { id } } } }'},
{query: 'query orders { orders { edges { node { id } } } }'},
],
}
render(<GraphiQLEditor config={configWithMultipleQueries} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]
const defaultTabs = graphiqlCall.defaultTabs

// First tab should always be WELCOME_MESSAGE
expect(defaultTabs[0].query).toContain('Welcome to GraphiQL')
})

test('passes correct props to GraphiQL', () => {
render(<GraphiQLEditor config={baseConfig} apiVersion="2024-10" />)

const graphiqlCall = mockGraphiQL.mock.calls[0][0]

expect(graphiqlCall.fetcher).toBeDefined()
expect(graphiqlCall.defaultEditorToolsVisibility).toBe(true)
expect(graphiqlCall.isHeadersEditorEnabled).toBe(false)
expect(graphiqlCall.forcedTheme).toBe('light')
expect(graphiqlCall.defaultTabs).toBeDefined()
expect(graphiqlCall.storage).toBeDefined()
})

test('updates fetcher when apiVersion changes', () => {
const {rerender} = render(<GraphiQLEditor config={baseConfig} apiVersion="2024-10" />)

expect(mockCreateFetcher).toHaveBeenCalledWith(
expect.objectContaining({
url: 'http://localhost:3457/graphiql/graphql.json?api_version=2024-10',
}),
)

// Clear mock and rerender with new version
mockCreateFetcher.mockClear()
rerender(<GraphiQLEditor config={baseConfig} apiVersion="2024-07" />)

// Note: Due to useMemo, the fetcher should recreate when apiVersion changes
expect(mockCreateFetcher).toHaveBeenCalledWith(
expect.objectContaining({
url: 'http://localhost:3457/graphiql/graphql.json?api_version=2024-07',
}),
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, {useMemo} from 'react'
import {GraphiQL} from 'graphiql'
import {createGraphiQLFetcher} from '@graphiql/toolkit'
import 'graphiql/style.css'
import type {GraphiQLConfig} from '@/types/config'
import {WELCOME_MESSAGE, DEFAULT_SHOP_QUERY} from '@/constants/defaultContent.ts'

interface GraphiQLEditorProps {
config: GraphiQLConfig
apiVersion: string
}

export function GraphiQLEditor({config, apiVersion}: GraphiQLEditorProps) {
// Create ephemeral storage to prevent localStorage tab caching
const ephemeralStorage: typeof localStorage = useMemo(() => {
return {
...localStorage,
getItem(key) {
// Always use defaultTabs
if (key === 'tabs') return null
return localStorage.getItem(key)
},
setItem(key, value) {
// Don't persist tabs
if (key === 'tabs') return
localStorage.setItem(key, value)
},
removeItem(key) {
localStorage.removeItem(key)
},
clear() {
localStorage.clear()
},
key(index) {
return localStorage.key(index)
},
get length() {
return localStorage.length
},
}
}, [])

// Create fetcher with current API version
const fetcher = useMemo(() => {
const url = `${config.baseUrl}/graphiql/graphql.json?api_version=${apiVersion}`

return createGraphiQLFetcher({
url,
headers: config.key
? {
Authorization: `Bearer ${config.key}`,
}
: {},
})
}, [config.baseUrl, config.key, apiVersion])

// Prepare default tabs
const defaultTabs = useMemo(() => {
const tabs = []

// 1. Add WELCOME_MESSAGE tab FIRST (in focus)
tabs.push({
query: WELCOME_MESSAGE,
})

// 2. Add DEFAULT_SHOP_QUERY tab SECOND (always)
tabs.push({
query: DEFAULT_SHOP_QUERY,
variables: '{}',
})

// 3. Add initial query from config (if provided)
if (config.query) {
tabs.push({
query: config.query,
variables: config.variables ?? '{}',
})
}

// 4. Add default queries from config
if (config.defaultQueries) {
config.defaultQueries.forEach(({query, variables, preface}) => {
tabs.push({
query: preface ? `${preface}\n${query}` : query,
variables: variables ?? '{}',
})
})
}

return tabs
}, [config.defaultQueries, config.query, config.variables])

return (
<GraphiQL
fetcher={fetcher}
defaultEditorToolsVisibility={true}
isHeadersEditorEnabled={false}
defaultTabs={defaultTabs}
forcedTheme="light"
storage={ephemeralStorage}
/>
)
}
Loading
Loading