Skip to content

Commit ffeaf36

Browse files
committed
feat(graphiql): complete GraphiQL section and app integration
Integrates all components into the main GraphiQL section with responsive layout. - Add main GraphiQL section with header and editor - Implement responsive layout with CSS Grid - Add API version selector, status badge, links, scopes note - Update App to use complete GraphiQL section - Include integration tests (219+ lines) Complete graphiql-console package is now functional All tests pass (89/89)
1 parent f574269 commit ffeaf36

File tree

5 files changed

+444
-1
lines changed

5 files changed

+444
-1
lines changed

packages/graphiql-console/src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
import {GraphiQLSection} from './sections/GraphiQL/index.ts'
2+
13
import React from 'react'
24

35
import {AppProvider} from '@shopify/polaris'
46
import '@shopify/polaris/build/esm/styles.css'
7+
import 'graphiql/style.css'
8+
import 'graphiql/setup-workers/vite'
59

610
function App() {
711
return (
812
<AppProvider i18n={{}}>
9-
<div>GraphiQL Console</div>
13+
<GraphiQLSection />
1014
</AppProvider>
1115
)
1216
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
.Container {
2+
display: flex;
3+
flex-direction: column;
4+
height: 100vh;
5+
width: 100%;
6+
}
7+
8+
.ErrorBanner {
9+
position: sticky;
10+
top: 0;
11+
z-index: 100;
12+
}
13+
14+
.Header {
15+
display: grid;
16+
grid-template-columns: 1fr; // Single column on narrow screens
17+
align-items: center;
18+
padding: 16px;
19+
border-bottom: 1px solid var(--p-color-border, #e1e3e5);
20+
background: #ffffff;
21+
gap: 16px;
22+
}
23+
24+
.LeftSection {
25+
display: flex;
26+
align-items: center;
27+
gap: 16px;
28+
flex-wrap: wrap;
29+
}
30+
31+
.RightSection {
32+
justify-self: start; // Left-align by default (single column on narrow screens)
33+
}
34+
35+
// Right-align only on wide screens with two-column layout
36+
@media only screen and (min-width: 1081px) {
37+
.RightSection {
38+
justify-self: end;
39+
}
40+
}
41+
42+
.StatusSection {
43+
display: flex;
44+
align-items: center;
45+
gap: 8px;
46+
}
47+
48+
.ControlsSection {
49+
display: flex;
50+
align-items: center;
51+
gap: 8px;
52+
}
53+
54+
.ScopesNote {
55+
display: inline-flex;
56+
align-items: center;
57+
height: 100%;
58+
}
59+
60+
.LinksSection {
61+
display: flex;
62+
align-items: center;
63+
gap: 8px;
64+
65+
a {
66+
line-height: 0;
67+
68+
&:hover .Polaris-Text--root {
69+
text-decoration: underline;
70+
}
71+
72+
span.Polaris-Text--root {
73+
max-width: max(12vw, 150px);
74+
text-overflow: ellipsis;
75+
overflow: hidden;
76+
white-space: nowrap;
77+
}
78+
}
79+
}
80+
81+
.GraphiQLContainer {
82+
flex-grow: 1;
83+
overflow: hidden;
84+
85+
// Ensure GraphiQL component fills container
86+
> div {
87+
height: 100%;
88+
}
89+
}
90+
91+
// Icon shrinking utility
92+
.with-shrunk-icon .Polaris-Icon {
93+
height: 1rem;
94+
width: 1rem;
95+
margin: 0.125rem;
96+
}
97+
98+
// Responsive design
99+
@media only screen and (max-width: 1550px) {
100+
.StatusSection,
101+
.ControlsSection,
102+
.LinksSection {
103+
// Hide labels like "Status:", "API version:", "Store:", "App:"
104+
:global(.top-bar-section-title) {
105+
display: none;
106+
}
107+
}
108+
}
109+
110+
@media only screen and (max-width: 1150px) {
111+
.LinksSection a span.Polaris-Text--root {
112+
max-width: max(12vw, 140px);
113+
}
114+
}
115+
116+
// Switch to two-column layout at wider screens
117+
@media only screen and (min-width: 1081px) {
118+
.Header {
119+
grid-template-columns: 7fr 5fr;
120+
}
121+
}
122+
123+
@media only screen and (max-width: 650px) {
124+
.LinksSection a span.Polaris-Text--root {
125+
max-width: 17vw;
126+
}
127+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import {GraphiQLSection} from './GraphiQL.tsx'
2+
import React from 'react'
3+
import {render, screen, fireEvent} from '@testing-library/react'
4+
import {describe, test, expect, vi, beforeEach} from 'vitest'
5+
import {AppProvider} from '@shopify/polaris'
6+
import type {ServerStatus} from '@/hooks/useServerStatus'
7+
8+
// Mock the hooks
9+
const mockUseServerStatus = vi.fn()
10+
vi.mock('@/hooks/useServerStatus', () => ({
11+
useServerStatus: (options: any) => mockUseServerStatus(options),
12+
}))
13+
14+
// Mock child components
15+
vi.mock('@/components/StatusBadge/StatusBadge.tsx', () => ({
16+
StatusBadge: ({status}: {status: ServerStatus}) => <div data-testid="status-badge">{JSON.stringify(status)}</div>,
17+
}))
18+
19+
vi.mock('@/components/ErrorBanner/ErrorBanner.tsx', () => ({
20+
ErrorBanner: ({isVisible}: {isVisible: boolean}) => (
21+
<div data-testid="error-banner" data-visible={isVisible}>
22+
ErrorBanner
23+
</div>
24+
),
25+
}))
26+
27+
vi.mock('@/components/LinkPills/LinkPills.tsx', () => ({
28+
LinkPills: ({status}: {status: ServerStatus}) => <div data-testid="link-pills">{JSON.stringify(status)}</div>,
29+
}))
30+
31+
vi.mock('@/components/ApiVersionSelector/ApiVersionSelector.tsx', () => ({
32+
ApiVersionSelector: ({
33+
versions,
34+
value,
35+
onChange,
36+
}: {
37+
versions: string[]
38+
value: string
39+
onChange: (version: string) => void
40+
}) => (
41+
<div data-testid="api-version-selector" data-versions={versions.join(',')} data-value={value}>
42+
<button onClick={() => onChange('new-version')}>Change Version</button>
43+
</div>
44+
),
45+
}))
46+
47+
vi.mock('@/components/GraphiQLEditor/GraphiQLEditor.tsx', () => ({
48+
GraphiQLEditor: ({config, apiVersion}: {config: any; apiVersion: string}) => (
49+
<div data-testid="graphiql-editor" data-api-version={apiVersion}>
50+
{JSON.stringify(config)}
51+
</div>
52+
),
53+
}))
54+
55+
// Helper to wrap components in AppProvider
56+
function renderWithProvider(element: React.ReactElement) {
57+
return render(<AppProvider i18n={{}}>{element}</AppProvider>)
58+
}
59+
60+
describe('<GraphiQLSection />', () => {
61+
beforeEach(() => {
62+
// Reset mocks before each test
63+
64+
// Default mock implementation
65+
mockUseServerStatus.mockReturnValue({
66+
serverIsLive: true,
67+
appIsInstalled: true,
68+
storeFqdn: 'test-store.myshopify.com',
69+
appName: 'Test App',
70+
appUrl: 'http://localhost:3000',
71+
})
72+
73+
// Mock window.__GRAPHIQL_CONFIG__
74+
;(window as any).__GRAPHIQL_CONFIG__ = undefined
75+
})
76+
77+
test('renders all child components', () => {
78+
renderWithProvider(<GraphiQLSection />)
79+
80+
expect(screen.getByTestId('status-badge')).toBeDefined()
81+
expect(screen.getByTestId('link-pills')).toBeDefined()
82+
expect(screen.getByTestId('api-version-selector')).toBeDefined()
83+
expect(screen.getByTestId('graphiql-editor')).toBeDefined()
84+
})
85+
86+
test('ErrorBanner visible when serverIsLive=false', () => {
87+
mockUseServerStatus.mockReturnValue({
88+
serverIsLive: false,
89+
appIsInstalled: true,
90+
})
91+
92+
renderWithProvider(<GraphiQLSection />)
93+
const errorBanner = screen.getByTestId('error-banner')
94+
95+
expect(errorBanner).toBeDefined()
96+
expect(errorBanner.getAttribute('data-visible')).toBe('true')
97+
})
98+
99+
test('ErrorBanner not rendered when serverIsLive=true', () => {
100+
mockUseServerStatus.mockReturnValue({
101+
serverIsLive: true,
102+
appIsInstalled: true,
103+
})
104+
105+
renderWithProvider(<GraphiQLSection />)
106+
107+
// ErrorBanner should not be in DOM when server is live
108+
expect(screen.queryByTestId('error-banner')).toBeNull()
109+
})
110+
111+
test('passes correct props to StatusBadge', () => {
112+
const mockStatus: ServerStatus = {
113+
serverIsLive: true,
114+
appIsInstalled: true,
115+
storeFqdn: 'test-store.myshopify.com',
116+
appName: 'Test App',
117+
appUrl: 'http://localhost:3000',
118+
}
119+
mockUseServerStatus.mockReturnValue(mockStatus)
120+
121+
renderWithProvider(<GraphiQLSection />)
122+
const statusBadge = screen.getByTestId('status-badge')
123+
124+
expect(statusBadge).toBeDefined()
125+
expect(statusBadge.textContent).toContain('"serverIsLive":true')
126+
expect(statusBadge.textContent).toContain('"appIsInstalled":true')
127+
})
128+
129+
test('passes correct props to LinkPills', () => {
130+
const mockStatus: ServerStatus = {
131+
serverIsLive: true,
132+
appIsInstalled: true,
133+
storeFqdn: 'test-store.myshopify.com',
134+
appName: 'Test App',
135+
appUrl: 'http://localhost:3000',
136+
}
137+
mockUseServerStatus.mockReturnValue(mockStatus)
138+
139+
renderWithProvider(<GraphiQLSection />)
140+
const linkPills = screen.getByTestId('link-pills')
141+
142+
expect(linkPills).toBeDefined()
143+
expect(linkPills.textContent).toContain('test-store.myshopify.com')
144+
})
145+
146+
test('getConfig() reads window.__GRAPHIQL_CONFIG__', () => {
147+
const customConfig = {
148+
baseUrl: 'http://localhost:4000',
149+
apiVersion: '2023-01',
150+
apiVersions: ['2023-01'],
151+
appName: 'Custom App',
152+
appUrl: 'http://localhost:3000',
153+
storeFqdn: 'custom.myshopify.com',
154+
}
155+
156+
;(window as any).__GRAPHIQL_CONFIG__ = customConfig
157+
158+
renderWithProvider(<GraphiQLSection />)
159+
const editor = screen.getByTestId('graphiql-editor')
160+
161+
expect(editor).toBeDefined()
162+
const editorConfig = JSON.parse(editor.textContent ?? '{}')
163+
expect(editorConfig.baseUrl).toBe('http://localhost:4000')
164+
expect(editorConfig.appName).toBe('Custom App')
165+
166+
// Cleanup
167+
;(window as any).__GRAPHIQL_CONFIG__ = undefined
168+
})
169+
170+
test('getConfig() falls back to defaults in development', () => {
171+
// Ensure no global config
172+
;(window as any).__GRAPHIQL_CONFIG__ = undefined
173+
174+
renderWithProvider(<GraphiQLSection />)
175+
const editor = screen.getByTestId('graphiql-editor')
176+
177+
expect(editor).toBeDefined()
178+
const editorConfig = JSON.parse(editor.textContent ?? '{}')
179+
180+
// Should have default values
181+
expect(editorConfig.apiVersion).toBe('2024-10')
182+
expect(editorConfig.apiVersions).toEqual(['2024-01', '2024-04', '2024-07', '2024-10', 'unstable'])
183+
})
184+
185+
test('ApiVersionSelector receives correct versions and value', () => {
186+
renderWithProvider(<GraphiQLSection />)
187+
const selector = screen.getByTestId('api-version-selector')
188+
189+
expect(selector).toBeDefined()
190+
expect(selector.getAttribute('data-versions')).toBe('2024-01,2024-04,2024-07,2024-10,unstable')
191+
expect(selector.getAttribute('data-value')).toBe('2024-10')
192+
})
193+
194+
test('calls useServerStatus with correct baseUrl', () => {
195+
renderWithProvider(<GraphiQLSection />)
196+
197+
expect(mockUseServerStatus).toHaveBeenCalledWith(
198+
expect.objectContaining({
199+
baseUrl: expect.any(String),
200+
}),
201+
)
202+
})
203+
204+
test('version selection updates GraphiQL editor', () => {
205+
renderWithProvider(<GraphiQLSection />)
206+
207+
// Initial state
208+
let editor = screen.getByTestId('graphiql-editor')
209+
expect(editor.getAttribute('data-api-version')).toBe('2024-10')
210+
211+
// Trigger version change
212+
const button = screen.getByText('Change Version')
213+
fireEvent.click(button)
214+
215+
// Re-find after state update
216+
editor = screen.getByTestId('graphiql-editor')
217+
expect(editor.getAttribute('data-api-version')).toBe('new-version')
218+
})
219+
})

0 commit comments

Comments
 (0)