Skip to content

Commit

Permalink
Implement real-time build status updates using SWR (#56)
Browse files Browse the repository at this point in the history
This PR introduces real-time build status updates for pull requests
using the SWR library. It enhances the user experience by providing live
updates without manual page refreshes.

Key changes:
- Integrated SWR for fetching and updating build statuses
- Improved UI to reflect running state with a spinning icon
- Updated GitHub webhook handler to trigger revalidations
- Enhanced `fetchBuildStatus` function in `github.ts` to support SWR
- Disabled action buttons while build is running
- Added periodic refresh of build status (every 10 seconds)
  • Loading branch information
slavingia authored Oct 12, 2024
2 parents d6339d1 + f06c345 commit 705921b
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 323 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ jobs:
- name: Install @vitest/coverage-v8
run: pnpm add -D @vitest/coverage-v8
- name: "Test"
run: npx vitest --coverage.enabled true
run: npx vitest run
333 changes: 93 additions & 240 deletions app/(dashboard)/dashboard/pull-request.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { PullRequestItem } from './pull-request';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { PullRequest, TestFile } from './types';
import { generateTestsResponseSchema } from "@/app/api/generate-tests/schema";

vi.mock('@/lib/github', () => ({
getPullRequestInfo: vi.fn(),
commitChangesToPullRequest: vi.fn(),
getFailingTests: vi.fn(),
}));
import { PullRequest } from './types';
import useSWR from 'swr';
import { fetchBuildStatus } from '@/lib/github';

vi.mock('@/lib/github', async (importOriginal) => {
const mod = await importOriginal();
return {
...mod,
getPullRequestInfo: vi.fn(),
commitChangesToPullRequest: vi.fn(),
getFailingTests: vi.fn(),
fetchBuildStatus: vi.fn(),
};
});

vi.mock('@/hooks/use-toast', () => ({
useToast: vi.fn(() => ({
Expand All @@ -27,6 +33,10 @@ vi.mock('react-diff-viewer', () => ({
default: () => <div data-testid="react-diff-viewer">Mocked Diff Viewer</div>,
}));

vi.mock('swr', () => ({
default: vi.fn(),
}));

describe('PullRequestItem', () => {
const mockPullRequest: PullRequest = {
id: 1,
Expand All @@ -48,6 +58,13 @@ describe('PullRequestItem', () => {
beforeEach(() => {
vi.clearAllMocks();
global.fetch = vi.fn();
vi.mocked(useSWR).mockReturnValue({
data: mockPullRequest,
mutate: vi.fn(),
error: undefined,
isValidating: false,
isLoading: false,
});
});

it('renders the pull request information correctly', () => {
Expand All @@ -57,192 +74,94 @@ describe('PullRequestItem', () => {
expect(screen.getByText('Build: success')).toBeInTheDocument();
});

it('handles "Write new tests" button click for successful build', async () => {
const { getPullRequestInfo } = await import('@/lib/github');
vi.mocked(getPullRequestInfo).mockResolvedValue({
diff: 'mock diff',
testFiles: [{ name: 'test.ts', content: 'test content' }],
});

vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ name: 'generated_test.ts', content: 'generated content' }]),
} as Response);

render(<PullRequestItem pullRequest={{ ...mockPullRequest, buildStatus: 'success' }} />);
const writeTestsButton = screen.getByText('Write new tests');
fireEvent.click(writeTestsButton);

await waitFor(() => {
expect(screen.getByText('Analyzing PR diff...')).toBeInTheDocument();
});
it('displays running build status', () => {
const runningPR = { ...mockPullRequest, buildStatus: 'running' };
vi.mocked(useSWR).mockReturnValue({
data: runningPR,
mutate: vi.fn(),
error: undefined,
isValidating: false,
isLoading: false,
});
render(<PullRequestItem pullRequest={runningPR} />);
expect(screen.getByText('Build: Running')).toBeInTheDocument();
expect(screen.getByText('Running...')).toBeInTheDocument();
});

await waitFor(() => {
expect(screen.getByText('generated_test.ts')).toBeInTheDocument();
expect(screen.getByTestId('react-diff-viewer')).toBeInTheDocument();
});
it('disables buttons when build is running', () => {
const runningPR = { ...mockPullRequest, buildStatus: 'running' };
vi.mocked(useSWR).mockReturnValue({
data: runningPR,
mutate: vi.fn(),
error: undefined,
isValidating: false,
isLoading: false,
});
render(<PullRequestItem pullRequest={runningPR} />);
expect(screen.getByText('Running...')).toBeDisabled();
});

it('handles "Update tests to fix" button click for failed build', async () => {
const failedPR = { ...mockPullRequest, buildStatus: 'failure' };
const { getPullRequestInfo, getFailingTests } = await import('@/lib/github');
vi.mocked(getPullRequestInfo).mockResolvedValue({
diff: 'mock diff',
testFiles: [
{ name: 'test1.ts', content: 'test content 1' },
{ name: 'test2.ts', content: 'test content 2' },
],
it('updates build status periodically', async () => {
const mutate = vi.fn();
const fetchBuildStatusMock = vi.fn().mockResolvedValue(mockPullRequest);
vi.mocked(fetchBuildStatus).mockImplementation(fetchBuildStatusMock);

vi.mocked(useSWR).mockImplementation((key, fetcher, options) => {
// Call the fetcher function to simulate SWR behavior
fetcher();
return {
data: mockPullRequest,
mutate,
error: undefined,
isValidating: false,
isLoading: false,
};
});
vi.mocked(getFailingTests).mockResolvedValue([
{ name: 'test1.ts', content: 'failing test content' },
]);

vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ name: 'test1.ts', content: 'fixed content' }]),
} as Response);

render(<PullRequestItem pullRequest={failedPR} />);
const updateTestsButton = screen.getByText('Update tests to fix');
fireEvent.click(updateTestsButton);

render(<PullRequestItem pullRequest={mockPullRequest} />);

await waitFor(() => {
expect(screen.getByText('Analyzing PR diff...')).toBeInTheDocument();
expect(useSWR).toHaveBeenCalledWith(
`pullRequest-${mockPullRequest.id}`,
expect.any(Function),
expect.objectContaining({
fallbackData: mockPullRequest,
refreshInterval: expect.any(Number),
onSuccess: expect.any(Function),
})
);
});

await waitFor(() => {
expect(screen.getByText('test1.ts')).toBeInTheDocument();
expect(screen.queryByText('test2.ts')).not.toBeInTheDocument();
expect(screen.getByTestId('react-diff-viewer')).toBeInTheDocument();
});
// Verify that fetchBuildStatus is called with the correct parameters
expect(fetchBuildStatusMock).toHaveBeenCalledWith(
mockPullRequest.repository.owner.login,
mockPullRequest.repository.name,
mockPullRequest.number
);
});

it('handles errors when generating tests', async () => {
const { getPullRequestInfo } = await import('@/lib/github');
it('triggers revalidation after committing changes', async () => {
const { getPullRequestInfo, commitChangesToPullRequest } = await import('@/lib/github');
vi.mocked(getPullRequestInfo).mockResolvedValue({
diff: 'mock diff',
testFiles: [{ name: 'test.ts', content: 'test content' }],
testFiles: [{ name: 'existing_test.ts', content: 'existing content' }],
});

vi.mocked(global.fetch).mockResolvedValue({
ok: false,
} as Response);

render(<PullRequestItem pullRequest={mockPullRequest} />);
const writeTestsButton = screen.getByText('Write new tests');
fireEvent.click(writeTestsButton);

await waitFor(() => {
expect(screen.getByText('Failed to generate test files.')).toBeInTheDocument();
});
});

it('handles committing changes', async () => {
const { commitChangesToPullRequest } = await import('@/lib/github');
vi.mocked(commitChangesToPullRequest).mockResolvedValue('https://github.com/commit/123');

vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ name: 'generated_test.ts', content: 'generated content' }]),
} as Response);

const { useToast } = await import('@/hooks/use-toast');
const mockToast = vi.fn();
vi.mocked(useToast).mockReturnValue({ toast: mockToast });

render(<PullRequestItem pullRequest={mockPullRequest} />);
const writeTestsButton = screen.getByText('Write new tests');
fireEvent.click(writeTestsButton);

await waitFor(() => {
expect(screen.getByText('generated_test.ts')).toBeInTheDocument();
});

const commitButton = screen.getByText('Commit changes');
fireEvent.click(commitButton);

await waitFor(() => {
expect(commitChangesToPullRequest).toHaveBeenCalled();
expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({
title: 'Changes committed successfully',
}));
});
});

it('handles canceling changes', async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ name: 'generated_test.ts', content: 'generated content' }]),
} as Response);

render(<PullRequestItem pullRequest={mockPullRequest} />);
const writeTestsButton = screen.getByText('Write new tests');
fireEvent.click(writeTestsButton);

await waitFor(() => {
expect(screen.getByText('generated_test.ts')).toBeInTheDocument();
});

const cancelButton = screen.getByText('Cancel');
fireEvent.click(cancelButton);

expect(screen.queryByText('generated_test.ts')).not.toBeInTheDocument();
});

it('handles file toggle', async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ name: 'generated_test.ts', content: 'generated content' }]),
} as Response);

render(<PullRequestItem pullRequest={mockPullRequest} />);
const writeTestsButton = screen.getByText('Write new tests');
fireEvent.click(writeTestsButton);

await waitFor(() => {
expect(screen.getByText('generated_test.ts')).toBeInTheDocument();
});

const checkbox = screen.getByRole('checkbox');
fireEvent.click(checkbox);

expect(checkbox).not.toBeChecked();
});

it('disables commit button when no files are selected', async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ name: 'generated_test.ts', content: 'generated content' }]),
} as Response);

render(<PullRequestItem pullRequest={mockPullRequest} />);
const writeTestsButton = screen.getByText('Write new tests');
fireEvent.click(writeTestsButton);

await waitFor(() => {
expect(screen.getByText('generated_test.ts')).toBeInTheDocument();
const mutate = vi.fn();
vi.mocked(useSWR).mockReturnValue({
data: mockPullRequest,
mutate,
error: undefined,
isValidating: false,
isLoading: false,
});

const checkbox = screen.getByRole('checkbox');
fireEvent.click(checkbox);

const commitButton = screen.getByText('Commit changes');
expect(commitButton).toBeDisabled();
});

it('handles errors when committing changes', async () => {
const { commitChangesToPullRequest } = await import('@/lib/github');
vi.mocked(commitChangesToPullRequest).mockRejectedValue(new Error('Commit failed'));

vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ name: 'generated_test.ts', content: 'generated content' }]),
} as Response);

const { useToast } = await import('@/hooks/use-toast');
const mockToast = vi.fn();
vi.mocked(useToast).mockReturnValue({ toast: mockToast });

render(<PullRequestItem pullRequest={mockPullRequest} />);
const writeTestsButton = screen.getByText('Write new tests');
fireEvent.click(writeTestsButton);
Expand All @@ -255,74 +174,8 @@ describe('PullRequestItem', () => {
fireEvent.click(commitButton);

await waitFor(() => {
expect(screen.getByText('Failed to commit changes. Please try again.')).toBeInTheDocument();
expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({
title: 'Error',
description: 'Failed to commit changes. Please try again.',
variant: 'destructive',
}));
});
});

it('displays pending build status', () => {
const pendingPR = { ...mockPullRequest, buildStatus: 'pending' };
render(<PullRequestItem pullRequest={pendingPR} />);
expect(screen.getByText('Build: pending')).toBeInTheDocument();
});

it('disables buttons when loading', async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => new Promise(resolve => setTimeout(() => resolve([]), 100)),
} as Response);

render(<PullRequestItem pullRequest={mockPullRequest} />);
const writeTestsButton = screen.getByText('Write new tests');
fireEvent.click(writeTestsButton);

await waitFor(() => {
expect(writeTestsButton).toBeDisabled();
});
});

it('handles committing changes with custom message', async () => {
const { commitChangesToPullRequest } = await import('@/lib/github');
vi.mocked(commitChangesToPullRequest).mockResolvedValue('https://github.com/commit/123');

vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ name: 'generated_test.ts', content: 'generated content' }]),
} as Response);

const { useToast } = await import('@/hooks/use-toast');
const mockToast = vi.fn();
vi.mocked(useToast).mockReturnValue({ toast: mockToast });

render(<PullRequestItem pullRequest={mockPullRequest} />);
const writeTestsButton = screen.getByText('Write new tests');
fireEvent.click(writeTestsButton);

await waitFor(() => {
expect(screen.getByText('generated_test.ts')).toBeInTheDocument();
});

const commitMessageInput = screen.getByPlaceholderText('Update test files');
fireEvent.change(commitMessageInput, { target: { value: 'Custom commit message' } });

const commitButton = screen.getByText('Commit changes');
fireEvent.click(commitButton);

await waitFor(() => {
expect(commitChangesToPullRequest).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
'Custom commit message'
);
expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({
title: 'Changes committed successfully',
}));
expect(commitChangesToPullRequest).toHaveBeenCalled();
expect(mutate).toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 705921b

Please sign in to comment.