Implement time off management (Issue #3) #12
30
web/package-lock.json
generated
30
web/package-lock.json
generated
@@ -8,7 +8,6 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-router-dom": "^7.13.1"
|
"react-router-dom": "^7.13.1"
|
||||||
@@ -835,12 +834,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/history": {
|
|
||||||
"version": "4.7.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
|
|
||||||
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.5.2",
|
"version": "25.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
|
||||||
@@ -855,6 +848,7 @@
|
|||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -870,27 +864,6 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-router": {
|
|
||||||
"version": "5.1.20",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
|
|
||||||
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/history": "^4.7.11",
|
|
||||||
"@types/react": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/react-router-dom": {
|
|
||||||
"version": "5.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
|
|
||||||
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/history": "^4.7.11",
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-router": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
||||||
@@ -1162,6 +1135,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-urls": {
|
"node_modules/data-urls": {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import { vi, type Mock } from 'vitest';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import TimeOff from './TimeOff';
|
import TimeOff from './TimeOff';
|
||||||
import { api, TimeOffRequest, ApiError } from '../api';
|
import { api, TimeOffRequest, ApiError } from '../api';
|
||||||
import { AuthProvider } from '../auth';
|
import { AuthProvider } from '../auth';
|
||||||
|
|
||||||
jest.mock('../api', () => {
|
vi.mock('../api', () => {
|
||||||
class MockApiError extends Error {
|
class MockApiError extends Error {
|
||||||
status: number;
|
status: number;
|
||||||
data: any;
|
data: any;
|
||||||
@@ -17,24 +18,24 @@ jest.mock('../api', () => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
api: {
|
api: {
|
||||||
listTimeOff: jest.fn(),
|
listTimeOff: vi.fn(),
|
||||||
createTimeOff: jest.fn(),
|
createTimeOff: vi.fn(),
|
||||||
updateTimeOff: jest.fn(),
|
updateTimeOff: vi.fn(),
|
||||||
deleteTimeOff: jest.fn(),
|
deleteTimeOff: vi.fn(),
|
||||||
reviewTimeOff: jest.fn(),
|
reviewTimeOff: vi.fn(),
|
||||||
getRemovedShifts: jest.fn(),
|
getRemovedShifts: vi.fn(),
|
||||||
listVolunteers: jest.fn(),
|
listVolunteers: vi.fn(),
|
||||||
},
|
},
|
||||||
ApiError: MockApiError,
|
ApiError: MockApiError,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockListTimeOff = api.listTimeOff as jest.Mock;
|
const mockListTimeOff = api.listTimeOff as Mock;
|
||||||
const mockCreateTimeOff = api.createTimeOff as jest.Mock;
|
const mockCreateTimeOff = api.createTimeOff as Mock;
|
||||||
const mockDeleteTimeOff = api.deleteTimeOff as jest.Mock;
|
const mockDeleteTimeOff = api.deleteTimeOff as Mock;
|
||||||
const mockReviewTimeOff = api.reviewTimeOff as jest.Mock;
|
const mockReviewTimeOff = api.reviewTimeOff as Mock;
|
||||||
const mockGetRemovedShifts = api.getRemovedShifts as jest.Mock;
|
const mockGetRemovedShifts = api.getRemovedShifts as Mock;
|
||||||
const mockListVolunteers = api.listVolunteers as jest.Mock;
|
const mockListVolunteers = api.listVolunteers as Mock;
|
||||||
|
|
||||||
function buildFakeJWT(payload: object): string {
|
function buildFakeJWT(payload: object): string {
|
||||||
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
||||||
@@ -90,7 +91,7 @@ function renderAsAdmin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
mockListVolunteers.mockResolvedValue([]);
|
mockListVolunteers.mockResolvedValue([]);
|
||||||
});
|
});
|
||||||
@@ -147,10 +148,10 @@ describe('TimeOff page', () => {
|
|||||||
|
|
||||||
it('shows conflict warning on 409 and allows confirmation', async () => {
|
it('shows conflict warning on 409 and allows confirmation', async () => {
|
||||||
mockListTimeOff.mockResolvedValue([]);
|
mockListTimeOff.mockResolvedValue([]);
|
||||||
const { ApiError: MockApiError } = jest.requireMock('../api');
|
const { ApiError: MockApiError } = await vi.importMock<typeof import('../api')>('../api');
|
||||||
mockCreateTimeOff
|
mockCreateTimeOff
|
||||||
.mockRejectedValueOnce(
|
.mockRejectedValueOnce(
|
||||||
new MockApiError('conflict', 409, {
|
new (MockApiError as any)('conflict', 409, {
|
||||||
message: 'Time off conflicts with assigned shifts.',
|
message: 'Time off conflicts with assigned shifts.',
|
||||||
conflicts: [
|
conflicts: [
|
||||||
{ instance_id: 100, name: 'Morning Walk', date: '2026-06-01', start_time: '08:00', end_time: '12:00' },
|
{ instance_id: 100, name: 'Morning Walk', date: '2026-06-01', start_time: '08:00', end_time: '12:00' },
|
||||||
|
|||||||
Reference in New Issue
Block a user