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
67 changes: 32 additions & 35 deletions packages/example-webpack/src/components/HookExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,36 @@ interface HookExampleProps {
*/
const HookExample: React.FC<HookExampleProps> = ({ serverUrl }) => {
const [name, setName] = useState('World');
const [triggerFetch, setTriggerFetch] = useState(0);

// Use the useGrpc hook for declarative data fetching
const { data, loading, error } = useGrpc({
serverUrl,
StubClass: GreetingServiceStub,
stubMethod: (stub) =>
stub.methods.greet({
name: name,
language: 'en',
options: {
style: 3, // FRIENDLY
include_timestamp: true,
metadata: {},
},
}),
deps: [name, triggerFetch],
onSuccess: (response) => {
console.log('=== Response Debug ===');
console.log('Full response:', response);
console.log('Timestamp:', response.timestamp, typeof response.timestamp);
console.log('Metadata:', response.metadata);
console.log('Metadata keys:', response.metadata ? Object.keys(response.metadata) : 'N/A');
console.log('Server version:', response.metadata?.serverVersion);
console.log('Request ID:', response.metadata?.requestId);
console.log('All response keys:', Object.keys(response));
// Use the useGrpc hook via stub method
const stub = new GreetingServiceStub({ serverUrl });
const { data, loading, error, refetch } = stub.useGreet(
{
name: name,
language: 'en',
options: {
style: 3, // FRIENDLY
include_timestamp: true,
metadata: {},
},
},
});
{
deps: [name],
onSuccess: (response) => {
console.log('=== Response Debug ===');
console.log('Full response:', response);
console.log('Timestamp:', response.timestamp, typeof response.timestamp);
console.log('Metadata:', response.metadata);
console.log('Metadata keys:', response.metadata ? Object.keys(response.metadata) : 'N/A');
console.log('Server version:', response.metadata?.serverVersion);
console.log('Request ID:', response.metadata?.requestId);
console.log('All response keys:', Object.keys(response));
},
}
);

const handleRefetch = () => {
setTriggerFetch((prev) => prev + 1);
refetch();
};

return (
Expand Down Expand Up @@ -115,15 +114,13 @@ const HookExample: React.FC<HookExampleProps> = ({ serverUrl }) => {
<div className="code-example">
<h3>Code Example:</h3>
<Highlight
code={`import { useGrpc } from '@hallow/react';
import { GreetingServiceStub } from './greeting.proto';
code={`import { GreetingServiceStub } from './greeting.proto';

const { data, loading, error, refetch } = useGrpc({
serverUrl: 'http://localhost:3000',
StubClass: GreetingServiceStub,
stubMethod: (stub) => stub.methods.greet({ name: 'World' }),
deps: [name]
});
const stub = new GreetingServiceStub({ serverUrl: 'http://localhost:3000' });
const { data, loading, error, refetch } = stub.useGreet(
{ name: 'World' },
{ deps: [name] }
);

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
Expand Down
40 changes: 18 additions & 22 deletions packages/example-webpack/src/components/SuspenseExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ interface SuspenseContentProps {
*/
const SuspenseContent: React.FC<SuspenseContentProps> = ({ serverUrl, name }) => {
// useSuspenseGrpc suspends rendering until data is ready
const data = useSuspenseGrpc({
serverUrl,
StubClass: GreetingServiceStub,
stubMethod: (stub) =>
stub.methods.greet({
name: name,
language: 'en',
options: {
style: 2, // FORMAL
include_timestamp: true,
metadata: {},
},
}),
// Provide explicit cache key that includes the name parameter
cacheKey: `${serverUrl}:greet:${name}`,
});
const stub = new GreetingServiceStub({ serverUrl });
const data = stub.useGreetSuspense(
{
name: name,
language: 'en',
options: {
style: 2, // FORMAL
include_timestamp: true,
metadata: {},
},
},
{
// Provide explicit cache key that includes the name parameter
cacheKey: `${serverUrl}:greet:${name}`,
}
);

return (
<div className="result">
Expand Down Expand Up @@ -148,15 +148,11 @@ const SuspenseExample: React.FC<SuspenseExampleProps> = ({ serverUrl }) => {
<h3>Code Example:</h3>
<Highlight
code={`import { Suspense } from 'react';
import { useSuspenseGrpc } from '@hallow/react';
import { GreetingServiceStub } from './greeting.proto';

function Content() {
const data = useSuspenseGrpc({
serverUrl: 'http://localhost:3000',
StubClass: GreetingServiceStub,
stubMethod: (stub) => stub.methods.greet({ name: 'World' })
});
const stub = new GreetingServiceStub({ serverUrl: 'http://localhost:3000' });
const data = stub.useGreetSuspense({ name: 'World' });

return <div>{data.reply}</div>;
}
Expand Down
117 changes: 62 additions & 55 deletions packages/example-webpack/src/components/__tests__/HookExample.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,19 @@ jest.mock('@hallow/react', () => ({

// Mock the proto import
jest.mock('../../proto/greeting.proto', () => ({
GreetingServiceStub: jest.fn(),
GreetingServiceStub: jest.fn().mockImplementation((config) => ({
config,
useGreet: jest.fn((request, options) => {
// Call the mock hook to simulate behavior and tracking
// We pass combined config similar to what generated code does
return mockUseGrpc({
serverUrl: config.serverUrl,
StubClass: 'GreetingServiceStub', // Just a marker
stubMethod: expect.any(Function),
...options
});
})
})),
}));

describe('HookExample', () => {
Expand All @@ -24,6 +36,7 @@ describe('HookExample', () => {
data: null,
loading: false,
error: null,
refetch: jest.fn(),
});
});

Expand Down Expand Up @@ -53,21 +66,19 @@ describe('HookExample', () => {
renderWithProviders(<HookExample serverUrl={mockServerUrl} />);

expect(screen.getByText('Code Example:')).toBeInTheDocument();
const codeBlock = screen.getByText(/useGrpc/);
const codeBlock = screen.getByText(/useGreet/);
expect(codeBlock).toBeInTheDocument();
});
});

describe('useGrpc Hook Integration', () => {
it('calls useGrpc hook with correct parameters', () => {
it('calls useGrpc hook via stub.useGreet', () => {
renderWithProviders(<HookExample serverUrl={mockServerUrl} />);

expect(mockUseGrpc).toHaveBeenCalledWith(
expect.any(Function), // stub loader
mockServerUrl,
expect.any(Function), // query function
['World', 0] // dependencies
);
expect(mockUseGrpc).toHaveBeenCalledWith(expect.objectContaining({
serverUrl: mockServerUrl,
deps: ['World']
}));
});

it('passes updated dependencies when name changes', async () => {
Expand All @@ -79,40 +90,31 @@ describe('HookExample', () => {
await user.type(input, 'Alice');

await waitFor(() => {
expect(mockUseGrpc).toHaveBeenCalledWith(
expect.any(Function),
mockServerUrl,
expect.any(Function),
expect.arrayContaining(['Alice'])
);
expect(mockUseGrpc).toHaveBeenCalledWith(expect.objectContaining({
deps: expect.arrayContaining(['Alice'])
}));
});
});

it('increments trigger when refetch button is clicked', async () => {
it('calls refetch function when button is clicked', async () => {
const user = setupUser();
// Mock refetch function
const mockRefetch = jest.fn();
mockUseGrpc.mockReturnValue({
data: null,
loading: false,
error: null,
refetch: mockRefetch,
});

renderWithProviders(<HookExample serverUrl={mockServerUrl} />);

const button = screen.getByRole('button', { name: /refetch/i });

// Initial call
expect(mockUseGrpc).toHaveBeenCalledWith(
expect.any(Function),
expect.any(String),
expect.any(Function),
['World', 0]
);

// Click refetch
await user.click(button);

await waitFor(() => {
expect(mockUseGrpc).toHaveBeenCalledWith(
expect.any(Function),
expect.any(String),
expect.any(Function),
['World', 1]
);
});
expect(mockRefetch).toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -170,7 +172,15 @@ describe('HookExample', () => {
});

describe('Success State', () => {
const mockData = mockSuccessResponse('World');
// Manually constructing mock data with camelCase keys to match HookExample expectations
const mockData = {
reply: 'Hello, World!',
timestamp: new Date().toISOString(),
metadata: {
serverVersion: '1.0.0',
requestId: 'test-request-id',
},
};

beforeEach(() => {
mockUseGrpc.mockReturnValue({
Expand Down Expand Up @@ -290,22 +300,25 @@ describe('HookExample', () => {
});
});

it('refetch button increments trigger counter', async () => {
it('calls refetch function when button is clicked', async () => {
const user = setupUser();
// Mock refetch function
const mockRefetch = jest.fn();
mockUseGrpc.mockReturnValue({
data: null,
loading: false,
error: null,
refetch: mockRefetch,
});

renderWithProviders(<HookExample serverUrl={mockServerUrl} />);

const button = screen.getByRole('button', { name: /refetch/i });

// Click multiple times
await user.click(button);
await user.click(button);
// Click refetch
await user.click(button);

// Verify trigger incremented in dependencies
await waitFor(() => {
const lastCall = mockUseGrpc.mock.calls[mockUseGrpc.mock.calls.length - 1];
expect(lastCall[3]).toEqual(['World', 3]);
});
expect(mockRefetch).toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -390,12 +403,9 @@ describe('HookExample', () => {
await user.clear(input);

await waitFor(() => {
expect(mockUseGrpc).toHaveBeenCalledWith(
expect.any(Function),
expect.any(String),
expect.any(Function),
['', expect.any(Number)]
);
expect(mockUseGrpc).toHaveBeenCalledWith(expect.objectContaining({
deps: ['']
}));
});
});

Expand All @@ -408,12 +418,9 @@ describe('HookExample', () => {
await user.type(input, "O'Brien <Test>");

await waitFor(() => {
expect(mockUseGrpc).toHaveBeenCalledWith(
expect.any(Function),
expect.any(String),
expect.any(Function),
expect.arrayContaining(["O'Brien <Test>"])
);
expect(mockUseGrpc).toHaveBeenCalledWith(expect.objectContaining({
deps: ["O'Brien <Test>"]
}));
});
});

Expand Down Expand Up @@ -441,7 +448,7 @@ describe('HookExample', () => {
data: {
reply: longMessage,
timestamp: new Date().toISOString(),
metadata: { server_version: '1.0.0', request_id: 'test' },
metadata: { serverVersion: '1.0.0', requestId: 'test' },
},
loading: false,
error: null,
Expand Down
Loading