-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add runtime PAT entry and user avatar #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
twoGiants marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| import { render, screen, waitFor } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { UserAvatar } from './UserAvatar'; | ||
| import { PAT_KEY, USER_KEY } from '../services/types'; | ||
| import { ForgeConnectionContext } from '../context/ForgeConnectionProvider'; | ||
| import { ReactNode } from 'react'; | ||
|
|
||
| jest.mock('react-i18next', () => ({ | ||
| useTranslation: () => ({ t: (key: string) => key }), | ||
| })); | ||
|
|
||
| const mockFetchUserInfo = jest.fn(); | ||
| jest.mock('../services/source-control/useSourceControlService', () => ({ | ||
| useSourceControlService: () => ({ | ||
| fetchUserInfo: mockFetchUserInfo, | ||
| }), | ||
| })); | ||
|
|
||
| const testUser = { name: 'twoGiants' }; | ||
|
|
||
| function renderWithContext( | ||
| ui: ReactNode, | ||
| contextValue = { isActive: false, connectToForge: jest.fn() }, | ||
| ) { | ||
| return render( | ||
| <ForgeConnectionContext.Provider value={contextValue}>{ui}</ForgeConnectionContext.Provider>, | ||
| ); | ||
| } | ||
|
|
||
| describe('UserAvatar', () => { | ||
| beforeEach(() => { | ||
| sessionStorage.clear(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| afterAll(() => { | ||
| sessionStorage.clear(); | ||
| }); | ||
|
|
||
| describe('rendering', () => { | ||
| it('renders "Connect to GitHub" when no user is stored', () => { | ||
| renderWithContext(<UserAvatar enableReconnect={false} />); | ||
|
|
||
| expect(screen.getByText('Connect to GitHub')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders username when user is stored in sessionStorage', () => { | ||
| sessionStorage.setItem(PAT_KEY, 'ghp_test'); | ||
| sessionStorage.setItem(USER_KEY, JSON.stringify(testUser)); | ||
|
|
||
| renderWithContext(<UserAvatar enableReconnect />); | ||
|
|
||
| expect(screen.getByText('twoGiants')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('button is clickable when enableReconnect is true', async () => { | ||
| const user = userEvent.setup(); | ||
| sessionStorage.setItem(PAT_KEY, 'ghp_test'); | ||
| sessionStorage.setItem(USER_KEY, JSON.stringify(testUser)); | ||
|
|
||
| renderWithContext(<UserAvatar enableReconnect />); | ||
|
|
||
| const button = screen.getByRole('button', { name: 'twoGiants' }); | ||
| await user.click(button); | ||
|
|
||
| expect(screen.getByText('Personal Access Token')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('button is disabled when enableReconnect is false', async () => { | ||
| const user = userEvent.setup(); | ||
|
|
||
| renderWithContext(<UserAvatar enableReconnect={false} />); | ||
|
|
||
| const button = screen.getByRole('button', { name: 'Connect to GitHub' }); | ||
| expect(button).toBeDisabled(); | ||
|
|
||
| await user.click(button); | ||
| expect(screen.queryByText('Personal Access Token')).not.toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('modal auto-open', () => { | ||
| it('opens modal automatically when enableReconnect is true and no PAT stored', () => { | ||
| renderWithContext(<UserAvatar enableReconnect />); | ||
|
|
||
| expect(screen.getByText('Personal Access Token')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('does not auto-open modal when PAT is already stored', () => { | ||
| sessionStorage.setItem(PAT_KEY, 'ghp_test'); | ||
| sessionStorage.setItem(USER_KEY, JSON.stringify(testUser)); | ||
|
|
||
| renderWithContext(<UserAvatar enableReconnect />); | ||
|
|
||
| expect(screen.queryByText('Personal Access Token')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('does not auto-open modal when enableReconnect is false', () => { | ||
| renderWithContext(<UserAvatar enableReconnect={false} />); | ||
|
|
||
| expect(screen.queryByText('Personal Access Token')).not.toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('PAT modal', () => { | ||
| it('Connect button disabled when input is empty', () => { | ||
| renderWithContext(<UserAvatar enableReconnect />); | ||
|
|
||
| expect(screen.getByRole('button', { name: 'Connect' })).toBeDisabled(); | ||
| }); | ||
|
|
||
| it('calls fetchUserInfo with PAT and updates UI on successful connect', async () => { | ||
| const user = userEvent.setup(); | ||
| const connectToForge = jest.fn(); | ||
| mockFetchUserInfo.mockResolvedValue(testUser); | ||
|
|
||
| renderWithContext(<UserAvatar enableReconnect />, { isActive: false, connectToForge }); | ||
|
|
||
| await user.type(screen.getByLabelText('Personal Access Token'), 'ghp_valid'); | ||
| await user.click(screen.getByRole('button', { name: 'Connect' })); | ||
|
|
||
| await waitFor(() => { | ||
| expect(mockFetchUserInfo).toHaveBeenCalledWith('ghp_valid'); | ||
| }); | ||
|
|
||
| expect(screen.getByText('twoGiants')).toBeInTheDocument(); | ||
| expect(sessionStorage.getItem(PAT_KEY)).toBe('ghp_valid'); | ||
| expect(JSON.parse(sessionStorage.getItem(USER_KEY)!)).toEqual(testUser); | ||
| expect(connectToForge).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('shows error alert when fetchUserInfo rejects', async () => { | ||
| const user = userEvent.setup(); | ||
| mockFetchUserInfo.mockRejectedValue(new Error('Bad credentials')); | ||
|
|
||
| renderWithContext(<UserAvatar enableReconnect />); | ||
|
|
||
| await user.type(screen.getByLabelText('Personal Access Token'), 'ghp_bad'); | ||
| await user.click(screen.getByRole('button', { name: 'Connect' })); | ||
|
|
||
| expect(await screen.findByText('Bad credentials')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('closes modal when Cancel is clicked', async () => { | ||
| const user = userEvent.setup(); | ||
|
|
||
| renderWithContext(<UserAvatar enableReconnect />); | ||
|
|
||
| expect(screen.getByText('Personal Access Token')).toBeInTheDocument(); | ||
|
|
||
| await user.click(screen.getByRole('button', { name: 'Cancel' })); | ||
|
|
||
| expect(screen.queryByText('Personal Access Token')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('disables Cancel button while validating', async () => { | ||
| const user = userEvent.setup(); | ||
| let resolveConnect: () => void; | ||
| mockFetchUserInfo.mockReturnValue( | ||
| new Promise<void>((resolve) => { | ||
| resolveConnect = resolve; | ||
| }), | ||
| ); | ||
|
|
||
| renderWithContext(<UserAvatar enableReconnect />); | ||
|
|
||
| await user.type(screen.getByLabelText('Personal Access Token'), 'ghp_slow'); | ||
| await user.click(screen.getByRole('button', { name: 'Connect' })); | ||
|
|
||
| expect(screen.getByRole('button', { name: 'Cancel' })).toBeDisabled(); | ||
|
|
||
| resolveConnect!(); | ||
| }); | ||
|
Comment on lines
+159
to
+176
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to resolve as we don't assert after resolution. You can remove |
||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets update this after approval.