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