diff --git a/spec/support/jasmine.js b/spec/support/jasmine.js index 2e20e97..9e116c8 100644 --- a/spec/support/jasmine.js +++ b/spec/support/jasmine.js @@ -15,6 +15,7 @@ require('@babel/register')({ '@/features': './src/assets/ts/features', '@/scss': './src/assets/scss', '@/types': './src/assets/ts/types', + '@/utils': './src/assets/ts/utils', '@/vendor': './src/lib/vendor', '@/test': './src/test' } diff --git a/src/assets/ts/components/popup-page.tsx b/src/assets/ts/components/popup-page.tsx new file mode 100644 index 0000000..886e36d --- /dev/null +++ b/src/assets/ts/components/popup-page.tsx @@ -0,0 +1,62 @@ +/* global EventListener */ + +import { useDispatch, useSelector } from 'react-redux'; +import { AppDispatch, RootState } from '@/store'; +import { signIn } from '@/features/popup-slice'; +import { useState, useEffect } from 'react'; +import { CustomEvent } from '@/types/core'; + +import { SignInForm } from '@/components/sign-in'; +import { AssetSaveSuccess } from '@/components/asset-save-success'; +import { AssetSaveFailure } from '@/components/asset-save-failure'; +import { Proposal } from '@/components/proposal'; +import { SignInSuccess } from '@/components/sign-in-success'; +import { Settings } from '@/components/settings'; + +export const PopupPage: React.FC = () => { + const dispatch = useDispatch(); + + const { + showSignIn, + showProposal, + showSuccess, + showSignInSuccess, + showSettings, + showAssetSaveFailure, + } = useSelector((state: RootState) => state.popup); + + const [assetDashboardLink, setAssetDashboardLink] = useState(''); + + useEffect(() => { + dispatch(signIn()); + + const handleAssetDashboardLink = ((event: CustomEvent) => { + setAssetDashboardLink(event.detail); + }) as EventListener; + + document.addEventListener( + 'set-asset-dashboard-link', + handleAssetDashboardLink, + ); + + return (): void => { + document.removeEventListener( + 'set-asset-dashboard-link', + handleAssetDashboardLink, + ); + }; + }, []); + + return ( + <> + {showSignIn && } + {showProposal && } + {showSuccess && ( + + )} + {showSignInSuccess && } + {showSettings && } + {showAssetSaveFailure && } + + ); +}; diff --git a/src/assets/ts/popup.tsx b/src/assets/ts/popup.tsx index 3ac490c..7f758fd 100644 --- a/src/assets/ts/popup.tsx +++ b/src/assets/ts/popup.tsx @@ -1,70 +1,11 @@ -/* global EventListener */ import ReactDOM from 'react-dom/client'; -import { useEffect, useState } from 'react'; -import { Provider, useDispatch, useSelector } from 'react-redux'; +import { Provider } from 'react-redux'; import 'bootstrap-icons/font/bootstrap-icons.scss'; import '@/scss/style.scss'; -import { SignInForm } from '@/components/sign-in'; -import { AssetSaveSuccess } from '@/components/asset-save-success'; -import { AssetSaveFailure } from '@/components/asset-save-failure'; -import { Proposal } from '@/components/proposal'; -import { SignInSuccess } from '@/components/sign-in-success'; -import { Settings } from '@/components/settings'; - import { store } from '@/store'; -import { signIn } from '@/features/popup-slice'; -import { RootState, AppDispatch } from '@/store'; -import { CustomEvent } from '@/types/core'; - -const PopupPage: React.FC = () => { - const dispatch = useDispatch(); - - const { - showSignIn, - showProposal, - showSuccess, - showSignInSuccess, - showSettings, - showAssetSaveFailure, - } = useSelector((state: RootState) => state.popup); - - const [assetDashboardLink, setAssetDashboardLink] = useState(''); - - useEffect(() => { - dispatch(signIn()); - - const handleAssetDashboardLink = ((event: CustomEvent) => { - setAssetDashboardLink(event.detail); - }) as EventListener; - - document.addEventListener( - 'set-asset-dashboard-link', - handleAssetDashboardLink, - ); - - return (): void => { - document.removeEventListener( - 'set-asset-dashboard-link', - handleAssetDashboardLink, - ); - }; - }, []); - - return ( - <> - {showSignIn && } - {showProposal && } - {showSuccess && ( - - )} - {showSignInSuccess && } - {showSettings && } - {showAssetSaveFailure && } - - ); -}; +import { PopupPage } from '@/components/popup-page'; const container = document.getElementById('app'); if (!container) throw new Error('Failed to find the app element'); diff --git a/src/test/spec/components/test-popup.tsx b/src/test/spec/components/test-popup.tsx new file mode 100644 index 0000000..e4fbbc7 --- /dev/null +++ b/src/test/spec/components/test-popup.tsx @@ -0,0 +1,99 @@ +import { render, fireEvent, waitFor, act } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { PopupPage } from '@/components/popup-page'; +import { configureStore } from '@reduxjs/toolkit'; +import popupReducer from '@/features/popup-slice'; +import * as popupSlice from '@/features/popup-slice'; + +describe('PopupPage', () => { + let store; + let originalFetch; + let mockBrowserStorage; + + beforeEach(() => { + store = configureStore({ + reducer: { + popup: popupReducer, + }, + }); + + originalFetch = global.fetch; + + // Mock browser storage + mockBrowserStorage = { + sync: { + set: jasmine.createSpy('set').and.resolveTo(), + }, + }; + global.browser = mockBrowserStorage; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('should initially render the sign-in form', () => { + render( + + + , + ); + + const signInForm = document.querySelector('#sign-in-page'); + + expect(signInForm).toBeTruthy(); + expect(signInForm?.querySelector('h3')?.textContent).toBe('Sign In'); + }); + + it('should show success page when sign-in is successful', async () => { + const mockFetch = jasmine.createSpy('fetch').and.resolveTo({ + status: 200, + json: () => Promise.resolve([]), + }); + global.fetch = mockFetch; + + const { rerender } = render( + + + , + ); + + const signInForm = document.querySelector('#sign-in-page'); + const tokenInput = signInForm?.querySelector('input[type="password"]'); + + expect(tokenInput).toBeTruthy(); + + await act(async () => { + fireEvent.change(tokenInput, { target: { value: 'test-token' } }); + }); + + const signInButton = signInForm?.querySelector('button'); + + await act(async () => { + fireEvent.click(signInButton); + }); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + // Manually dispatch the notifySignInSuccess action to update the Redux state + await act(async () => { + store.dispatch(popupSlice.notifySignInSuccess()); + }); + + // Force a re-render of the component + rerender( + + + , + ); + + // Wait for the success page to appear + await waitFor(() => { + const successPage = document.querySelector('#success-page'); + + expect(successPage).toBeTruthy(); + }); + }); +}); diff --git a/src/test/spec/components/test-sign-in-form.tsx b/src/test/spec/components/test-sign-in-form.tsx new file mode 100644 index 0000000..399fe52 --- /dev/null +++ b/src/test/spec/components/test-sign-in-form.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import { SignInForm } from '@/components/sign-in'; +import popupReducer from '@/features/popup-slice'; + +describe('SignInForm', () => { + let store; + let mockBrowserStorage; + let originalFetch; + + beforeEach(() => { + store = configureStore({ + reducer: { + popup: popupReducer, + }, + }); + + mockBrowserStorage = { + sync: { + set: jasmine.createSpy('set').and.resolveTo(), + }, + }; + global.browser = mockBrowserStorage; + + // Save the original fetch function + originalFetch = global.fetch; + }); + + afterEach(() => { + // Restore the original fetch function + global.fetch = originalFetch; + }); + + it('renders the sign-in form correctly', () => { + render( + + + , + ); + + const headingText = document.querySelector('h3')?.textContent; + const passwordInput = document.querySelector('input[type="password"]'); + const passwordInputPlaceholder = passwordInput?.getAttribute('placeholder'); + const submitButton = document.querySelector('button[type="submit"]'); + + expect(headingText).toBe('Sign In'); + expect(passwordInput).toBeTruthy(); + expect(passwordInputPlaceholder).toBe('Enter token'); + expect(submitButton).toBeTruthy(); + }); + + it('should call fetch when the sign in button is clicked', async () => { + const mockFetch = jasmine.createSpy('fetch').and.resolveTo({ + status: 200, + json: () => Promise.resolve([]), + }); + global.fetch = mockFetch; + + render( + + + , + ); + + const tokenInput = document.querySelector( + 'input[type="password"]', + ) as HTMLInputElement; + const submitButton = document.querySelector('button[type="submit"]'); + + fireEvent.change(tokenInput, { target: { value: 'test-token' } }); + + expect(tokenInput.value).toBe('test-token'); + + fireEvent.click(submitButton); + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + const spinner = submitButton?.querySelector('.spinner'); + const label = submitButton?.querySelector('.label'); + + expect(spinner).toBeFalsy(); + expect(label).toBeTruthy(); + expect(label?.textContent).toBe('Sign In'); + }); + }); +});