From a7f25cbdfdab6f62dc2a08e3b756358eede8c0a4 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Thu, 13 Mar 2025 09:56:57 -0700 Subject: [PATCH] chore: improve CI and fix navigation (#352) * chore: simple change to trigger new ci * feat: make PR CD work issue #338 * ci: fix target deploy url * ci: improve deployments into just one matrix job * ci: put secret on env * fix: fix nagivation when creating notes and tasks from the home page * test: add sidebar test cases * chore: test files cleanup --- .github/workflows/pr.yml | 115 +++++------------- client/src/App.tsx | 2 +- client/src/__test__/App.test.tsx | 12 +- .../components/ContentHeader.test.tsx | 2 +- .../src/__test__/components/Header.test.tsx | 2 +- .../src/__test__/components/Sidebar.test.tsx | 10 +- .../__test__/context/AuthProvider.test.tsx | 1 - .../__test__/context/SidebarProvider.test.tsx | 66 ++++++++++ .../__test__/utils/TranslatorUtils.test.ts | 1 + client/src/__test__/views/About.test.tsx | 1 - client/src/__test__/views/Account.test.tsx | 5 +- client/src/__test__/views/Home.test.tsx | 45 ++++--- client/src/__test__/views/NoteAdd.test.tsx | 6 +- client/src/__test__/views/TaskAdd.test.tsx | 6 +- client/src/components/ContentHeader/index.tsx | 13 +- client/src/components/Sidebar/index.tsx | 53 +++----- client/src/context/SidebarContext.ts | 10 ++ client/src/context/SidebarProvider.tsx | 27 ++++ client/src/index.tsx | 5 +- client/src/views/NoteAdd/index.tsx | 4 +- client/src/views/TaskAdd/index.tsx | 4 +- .../controller/AuthenticationController.java | 2 +- 22 files changed, 232 insertions(+), 160 deletions(-) create mode 100644 client/src/__test__/context/SidebarProvider.test.tsx create mode 100644 client/src/context/SidebarContext.ts create mode 100644 client/src/context/SidebarProvider.tsx diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e4e46c3..8643372 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -103,62 +103,6 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - client-dokploy: - name: Deploy Client Changes to Stage - runs-on: ubuntu-latest - needs: client-docker-build - if: github.event.pull_request.user.login == 'rmcampos' && github.event_name == 'pull_request' - env: - STAGE_DOMAIN: ${{ vars.STAGE_DOMAIN }} - API_KEY: ${{ secrets.DOKPLOY_API_KEY }} - APP_ID: ${{ secrets.STAGE_WEB_CLIENT_ID }} - steps: - - name: Pre-deployment check - run: | - if ! curl -s -f "${STAGE_DOMAIN}/"; then - echo "Stage environment is not healthy" - else - echo "Stage environment is healthy" - fi - - - name: Trigger Dokploy Deployment - uses: nick-fields/retry@v3.0.2 - with: - timeout_minutes: 2 - max_attempts: 3 - command: | - response=$(curl -X POST \ - "${STAGE_DOMAIN}/api/application.deploy" \ - --max-time 30 \ - -H "accept: application/json" \ - -H "x-api-key: ${API_KEY}" \ - -H "Content-Type: application/json" \ - -d "{\"json\":{\"applicationId\":\"${APP_ID}\"}}" \ - -w "\n%{http_code}" \ - -s) - - status_code=$(echo "$response" | tail -n1) - body=$(echo "$response" | sed '$d') - - echo "Status code: $status_code" - echo "Response body: $body" - - if [ "$status_code" -ge 400 ]; then - echo "Deployment failed with status code $status_code" - exit 1 - fi - - name: Verify deployment - run: | - # Wait for deployment to complete - sleep 30 - - if ! curl -s -f "${STAGE_DOMAIN}/"; then - echo "Stage environment is not healthy" - exit 1 - fi - - echo "Health check passed." - java-code-checks: needs: changes if: ${{ needs.changes.outputs.server == 'true' }} @@ -243,65 +187,72 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - server-dokploy: - name: Deploy API Changes to Stage + stage-deployments: + name: Deploy Changes to Stage runs-on: ubuntu-latest - needs: client-docker-build + needs: [java-docker-build, client-docker-build] if: github.event.pull_request.user.login == 'rmcampos' && github.event_name == 'pull_request' env: - STAGE_DOMAIN: ${{ vars.STAGE_DOMAIN }} - STAGE_API_DOMAIN: ${{ vars.STAGE_API_DOMAIN }} + DEPLOY_DOMAIN: ${{ vars.DEPLOY_DOMAIN }} API_KEY: ${{ secrets.DOKPLOY_API_KEY }} - APP_ID: ${{ secrets.STAGE_API_CLIENT_ID }} + SERVER_APPID: ${{ secrets.STAGE_API_CLIENT_ID }} + CLIENT_APPID: ${{ secrets.STAGE_WEB_CLIENT_ID }} + strategy: + matrix: + name: [server, client] + include: + - name: server + health_check_url: "${{ vars.API_STAGE_URL }}/actuator/health" + app_id: ${SERVER_APPID} + - name: client + health_check_url: "${{ vars.CLIENT_STAGE_URL }}/" + app_id: ${CLIENT_APPID} steps: - name: Pre-deployment check run: | - response=$(curl -s "${STAGE_API_DOMAIN}/actuator/health") - status=$(echo "$response" | jq -r '.status') - if [ "$status" != "UP" ]; then - echo "Stage environment is not healthy. Status: $status" + if ! curl -s -f "${{ matrix.health_check_url }}"; then + echo "Stage environment is not healthy" else - echo "Stage environment is healthy!" + echo "Stage environment is healthy" fi - - name: Trigger Dokploy Deployment + - name: Trigger Deployment uses: nick-fields/retry@v3.0.2 with: timeout_minutes: 2 max_attempts: 3 command: | response=$(curl -X POST \ - "${STAGE_DOMAIN}/api/application.deploy" \ + "${DEPLOY_DOMAIN}/api/application.deploy" \ --max-time 30 \ -H "accept: application/json" \ -H "x-api-key: ${API_KEY}" \ -H "Content-Type: application/json" \ - -d "{\"json\":{\"applicationId\":\"${APP_ID}\"}}" \ + -d "{\"applicationId\":\"${{ matrix.app_id }}\"}" \ -w "\n%{http_code}" \ -s) status_code=$(echo "$response" | tail -n1) - body=$(echo "$response" | sed '$d') - echo "Status code: $status_code" - echo "Response body: $body" if [ "$status_code" -ge 400 ]; then + body=$(echo "$response" | sed '$d') + echo "Deployment failed with status code $status_code" + echo "Response body: $body" exit 1 + else + echo "Deployment succeeded!" fi + - name: Verify deployment run: | # Wait for deployment to complete sleep 30 - response=$(curl -s "${STAGE_API_DOMAIN}/actuator/health") - status=$(echo "$response" | jq -r '.status') - - if [ "$status" != "UP" ]; then - echo "Stage environment is not healthy. Status: $status" - echo "Full response: $response" + if ! curl -s -f "${{ matrix.health_check_url }}"; then + echo "Stage environment is not healthy" exit 1 - fi - - echo "Health check passed. Status: $status" \ No newline at end of file + else + echo "Stage environment is healthy" + fi \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 76587f5..8b0d1f3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -26,7 +26,7 @@ function App(): React.ReactNode { const { signed, checkCurrentAuthUser } = useContext(AuthContext); /** - * Routes for users who are not signed in. + * Routes for the users who are not signed in. * @type {RouteObject[]} */ const notSignedRouter: RouteObject[] = [ diff --git a/client/src/__test__/App.test.tsx b/client/src/__test__/App.test.tsx index 8a312b8..3e0f4b3 100644 --- a/client/src/__test__/App.test.tsx +++ b/client/src/__test__/App.test.tsx @@ -1,14 +1,22 @@ import React from 'react'; -import { test } from 'vitest'; +import { test, vi } from 'vitest'; import App from '../App'; import { render } from '@testing-library/react'; import AuthContext from '../context/AuthContext'; import authContextMock from './__mocks__/authContextMock'; +import SidebarContext from '../context/SidebarContext'; + +const sidebarContextMock = { + currentPage: '/home', + setNewPage: vi.fn() +}; test('Renders the app', () => { render( - + + + ); }); diff --git a/client/src/__test__/components/ContentHeader.test.tsx b/client/src/__test__/components/ContentHeader.test.tsx index 9802dd2..ef9a5f8 100644 --- a/client/src/__test__/components/ContentHeader.test.tsx +++ b/client/src/__test__/components/ContentHeader.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { MemoryRouter } from 'react-router'; import ContentHeader from '../../components/ContentHeader'; diff --git a/client/src/__test__/components/Header.test.tsx b/client/src/__test__/components/Header.test.tsx index 1ec7d50..fcccecf 100644 --- a/client/src/__test__/components/Header.test.tsx +++ b/client/src/__test__/components/Header.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; import Header from '../../components/Header'; import '../../i18n'; diff --git a/client/src/__test__/components/Sidebar.test.tsx b/client/src/__test__/components/Sidebar.test.tsx index 65de1b6..1bfe75b 100644 --- a/client/src/__test__/components/Sidebar.test.tsx +++ b/client/src/__test__/components/Sidebar.test.tsx @@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router'; import { I18nextProvider } from 'react-i18next'; import Sidebar from '../../components/Sidebar'; import AuthContext from '../../context/AuthContext'; +import SidebarContext from '../../context/SidebarContext'; import i18n from '../../i18n'; const authContextMock = { @@ -25,13 +26,20 @@ const authContextMock = { updateUser: vi.fn(), }; +const sidebarContextMock = { + currentPage: '/home', + setNewPage: vi.fn() +}; + describe('Sidebar Component', () => { const renderSidebar = () => { return render( - + + + diff --git a/client/src/__test__/context/AuthProvider.test.tsx b/client/src/__test__/context/AuthProvider.test.tsx index 481f79c..4375663 100644 --- a/client/src/__test__/context/AuthProvider.test.tsx +++ b/client/src/__test__/context/AuthProvider.test.tsx @@ -6,7 +6,6 @@ import userEvent from '@testing-library/user-event'; import AuthProvider from '../../context/AuthProvider'; import AuthContext, { AuthContextData } from '../../context/AuthContext'; import api from '../../api-service/api'; -import ApiConfig from '../../api-service/apiConfig'; import { API_TOKEN, USER_DATA } from '../../app-constants/app-constants'; // Mock the API service methods. diff --git a/client/src/__test__/context/SidebarProvider.test.tsx b/client/src/__test__/context/SidebarProvider.test.tsx new file mode 100644 index 0000000..d29677b --- /dev/null +++ b/client/src/__test__/context/SidebarProvider.test.tsx @@ -0,0 +1,66 @@ +// AuthProvider.test.tsx +import React, { useContext } from 'react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, act, waitFor, cleanup } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import SidebarProvider from '../../context/SidebarProvider'; +import SidebarContext, { SidebarContextData } from '../../context/SidebarContext'; + +// Create a helper component to consume AuthContext for testing. +const ConsumerComponent: React.FC = () => { + const { + currentPage, + setNewPage + } = useContext(SidebarContext); + + return ( +
+
{currentPage}
+ +
+ ); +}; + +describe('SidebarProvider', () => { + // Reset DOM and mocks for each test. + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + cleanup(); + }); + + it('should render the default context values', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('page').textContent).toBe('/home'); + }); + + it('should set a new page after click', async () => { + const { getByTestId } = render( + + + + ); + + await act(async () => { + userEvent.click(getByTestId('setPage')); + }); + + await waitFor(() => + expect(getByTestId('page').textContent).toBe('/another') + ); + }); +}); diff --git a/client/src/__test__/utils/TranslatorUtils.test.ts b/client/src/__test__/utils/TranslatorUtils.test.ts index e93256c..8170516 100644 --- a/client/src/__test__/utils/TranslatorUtils.test.ts +++ b/client/src/__test__/utils/TranslatorUtils.test.ts @@ -11,6 +11,7 @@ const tasks: TaskResponse[] = [ dueDate: '', dueDateFmt: '', lastUpdate: 'Moments ago', + tag: 'test', urls: [] } ]; diff --git a/client/src/__test__/views/About.test.tsx b/client/src/__test__/views/About.test.tsx index 53a5cfa..aa895cf 100644 --- a/client/src/__test__/views/About.test.tsx +++ b/client/src/__test__/views/About.test.tsx @@ -1,5 +1,4 @@ import React from 'react'; -// import { MemoryRouter } from 'react-router'; import { render } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import About from '../../views/About'; diff --git a/client/src/__test__/views/Account.test.tsx b/client/src/__test__/views/Account.test.tsx index 2c9d89c..221178c 100644 --- a/client/src/__test__/views/Account.test.tsx +++ b/client/src/__test__/views/Account.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, fireEvent, waitFor, getByTestId } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { I18nextProvider } from 'react-i18next'; import { MemoryRouter } from 'react-router'; @@ -35,7 +35,8 @@ const authContextMock = { name: 'Ricardo', email: 'test@example.com', admin: false, - createdAt: new Date() + createdAt: new Date(), + gravatarImageUrl: 'http://image.com' }, checkCurrentAuthUser: vi.fn(), signIn: vi.fn(), diff --git a/client/src/__test__/views/Home.test.tsx b/client/src/__test__/views/Home.test.tsx index 1aa69c2..edc8654 100644 --- a/client/src/__test__/views/Home.test.tsx +++ b/client/src/__test__/views/Home.test.tsx @@ -1,8 +1,9 @@ import React from 'react'; -// import { MemoryRouter } from 'react-router'; -import { render } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { MemoryRouter } from 'react-router'; +import { SummaryResponse } from '../../types/SummaryResponse'; +import api from '../../api-service/api'; import Home from '../../views/Home'; import '../../i18n'; import AuthContext from '../../context/AuthContext'; @@ -21,30 +22,40 @@ const authContextMock = { name: 'Ricardo', email: 'ricardo@campos.com', admin: false, - createdAt: new Date() + createdAt: new Date(), + gravatarImageUrl: 'http://image.com' }, checkCurrentAuthUser: vi.fn(), signIn: vi.fn(), signOut: vi.fn(), register: vi.fn(), isAdmin: false, - updateUser: vi.fn(), + updateUser: vi.fn() }; describe('Renders the home view', () => { - it('should render text based on new contentHeader component', () => { - const { getByText } = render( - - - - - - ); + it('should render text based on new contentHeader component', async () => { + const mockData: SummaryResponse = { + pendingTaskCount: 354, + doneTaskCount: 555, + notesCount: 2222 + }; + const mockedGetJSON = vi.spyOn(api, 'getJSON').mockResolvedValue(mockData); - expect(getByText('Hello,')).toBeDefined(); - expect(getByText('Ricardo')).toBeDefined(); - expect(getByText('Welcome to TaskNote! Get ready to complete your pending tasks')).toBeDefined(); - expect(getByText('Start Your Day, Be')).toBeDefined(); - expect(getByText('Productive')).toBeDefined(); + await act(async () => { + render( + + + + + + ); + }); + + expect(screen.getByText('Hello,')).toBeDefined(); + expect(screen.getByText('Ricardo')).toBeDefined(); + expect(screen.getByText('Welcome to TaskNote! Get ready to complete your pending tasks')).toBeDefined(); + expect(screen.getByText('Start Your Day, Be')).toBeDefined(); + expect(screen.getByText('Productive')).toBeDefined(); }); }); diff --git a/client/src/__test__/views/NoteAdd.test.tsx b/client/src/__test__/views/NoteAdd.test.tsx index 287b924..11d5ed4 100644 --- a/client/src/__test__/views/NoteAdd.test.tsx +++ b/client/src/__test__/views/NoteAdd.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, fireEvent, waitFor, getByText, getByTestId } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { MemoryRouter } from 'react-router'; import { I18nextProvider } from 'react-i18next'; @@ -99,8 +99,8 @@ describe('NoteAdd Component', () => { it('should render text based on new contentHeader component', () => { const { getByText } = renderNoteAdd(); - expect(getByText('All')).toBeDefined(); - expect(getByText('Notes')).toBeDefined(); + expect(getByText('Add')).toBeDefined(); + expect(getByText('Note')).toBeDefined(); expect(getByText('Save your notes in plain text or Markdown format')).toBeDefined(); expect(getByText('Create, Filter, and Easily Find')).toBeDefined(); expect(getByText('Them')).toBeDefined(); diff --git a/client/src/__test__/views/TaskAdd.test.tsx b/client/src/__test__/views/TaskAdd.test.tsx index fa61026..36cd0f3 100644 --- a/client/src/__test__/views/TaskAdd.test.tsx +++ b/client/src/__test__/views/TaskAdd.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, fireEvent, waitFor, getByText } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { MemoryRouter } from 'react-router'; import { I18nextProvider } from 'react-i18next'; @@ -100,8 +100,8 @@ describe('TaskAdd Component', () => { it('should render text based on new contentHeader component', () => { const { getByText } = renderTaskAdd(); - expect(getByText('All')).toBeDefined(); - expect(getByText('Tasks')).toBeDefined(); + expect(getByText('Add')).toBeDefined(); + expect(getByText('Task')).toBeDefined(); expect(getByText('Be on top of your TODO list')).toBeDefined(); expect(getByText('Create, Filter, and Easily Find')).toBeDefined(); expect(getByText('Them')).toBeDefined(); diff --git a/client/src/components/ContentHeader/index.tsx b/client/src/components/ContentHeader/index.tsx index 0212690..6558cd2 100644 --- a/client/src/components/ContentHeader/index.tsx +++ b/client/src/components/ContentHeader/index.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { Col, Row } from 'react-bootstrap'; import { PlusCircleFill } from 'react-bootstrap-icons'; import { NavLink } from 'react-router'; +import SidebarContext from '../../context/SidebarContext'; type Props = { h1TextRegular: string; @@ -24,6 +25,8 @@ type Props = { * @returns {React.ReactNode} The rendered ContentHeader component. */ const ContentHeader: React.FC = (props: Props): React.ReactNode => { + const { setNewPage } = useContext(SidebarContext); + return ( <>

@@ -47,22 +50,22 @@ const ContentHeader: React.FC = (props: Props): React.ReactNode => { {props.isHomeComponent && ( - + setNewPage('/tasks/new')}> - + setNewPage('/notes/new')}> diff --git a/client/src/components/Sidebar/index.tsx b/client/src/components/Sidebar/index.tsx index 4b1da7d..7e7308e 100644 --- a/client/src/components/Sidebar/index.tsx +++ b/client/src/components/Sidebar/index.tsx @@ -1,8 +1,9 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; import { Nav } from 'react-bootstrap'; import { NavLink } from 'react-router'; import { useTranslation } from 'react-i18next'; import AuthContext from '../../context/AuthContext'; +import SidebarContext from '../../context/SidebarContext'; import NavButton from '../NavButton'; import SidebarIcon from '../SidebarIcon'; import { env } from '../../env'; @@ -15,28 +16,12 @@ import './style.css'; */ function Sidebar(): React.ReactNode { const { signOut, user } = useContext(AuthContext); + const { currentPage, setNewPage } = useContext(SidebarContext); const { t } = useTranslation(); const build = `Build: ${env.VITE_BUILD}`; - const [current, setCurrent] = useState('/home'); // Note: when selected, change class to plus-jakarta-sans-thin and add background - /** - * Handles the sign-out action. - */ - const goOut = (): void => { - signOut(); - }; - - /** - * Handles the navigation link click event. - * - * @param {string} menu - The menu path. - */ - const navLinkClicked = (menu: string): void => { - setCurrent(menu); - }; - useEffect(() => {}, [user]); return ( @@ -51,29 +36,29 @@ function Sidebar(): React.ReactNode {
Main Menu