Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 21 additions & 23 deletions apps/container/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,51 +16,49 @@
"tsc": "tsc --noEmit"
},
"dependencies": {
"@amcharts/amcharts5": "5.15.1",
"@amcharts/amcharts5": "5.15.6",
"@mdi/font": "7.4.47",
"@tanstack/query-core": "5.90.16",
"@tanstack/vue-query": "5.92.5",
"@tanstack/vue-query-devtools": "5.91.0",
"@tanstack/query-core": "5.90.20",
"@tanstack/vue-query": "5.92.9",
"@tanstack/vue-query-devtools": "6.1.5",
"@ufabc-next/services": "workspace:*",
"@vee-validate/zod": "4.15.1",
"@vueuse/core": "^13.9.0",
"axios": "1.13.2",
"@vueuse/core": "^14.2.1",
"axios": "1.13.5",
"dayjs": "1.11.19",
"element-plus": "2.13.1",
"element-plus": "2.13.2",
"highcharts": "11.4.8",
"highcharts-vue": "1.4.3",
"ics": "^3.5.0",
"lodash.debounce": "4.0.8",
"mixpanel-browser": "^2.65.0",
"mixpanel-browser": "^2.74.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"vee-validate": "4.15.1",
"vue": "3.5.26",
"vue-router": "4.6.4",
"vue-zustand": "0.6.0",
"vuetify": "3.11.6",
"vue": "3.5.28",
"vue-router": "5.0.2",
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a major version upgrade from vue-router 4.6.4 to 5.0.2. Major version upgrades typically include breaking changes. The code changes in AppBar.vue (lines 164, 170, 181) suggest handling cases where router might be undefined, which indicates awareness of potential breaking changes. However, please ensure all vue-router usage throughout the codebase has been reviewed for compatibility with version 5.x breaking changes, particularly around router initialization and type definitions.

Suggested change
"vue-router": "5.0.2",
"vue-router": "4.6.4",

Copilot uses AI. Check for mistakes.
"vuetify": "3.11.8",
"zod": "3.25.76"
},
"devDependencies": {
"@testing-library/jest-dom": "6.9.1",
"@testing-library/user-event": "14.6.1",
"@testing-library/vue": "8.1.0",
"@types/lodash.debounce": "4.0.9",
"@types/mixpanel-browser": "^2.60.0",
"@types/node": "^24.2.1",
"@types/node": "^25.2.3",
"@ufabc-next/types": "workspace:*",
"@vitejs/plugin-vue": "^6.0.1",
"@vitest/coverage-v8": "3.2.4",
"globals": "^16.3.0",
"@vitejs/plugin-vue": "^6.0.4",
"@vitest/coverage-v8": "4.0.18",
"globals": "^17.3.0",
"husky": "9.1.7",
"jsdom": "26.1.0",
"msw": "^2.8.4",
"sass": "1.97.2",
"sass-loader": "16.0.6",
"jsdom": "28.1.0",
"msw": "^2.12.10",
"sass": "1.97.3",
"sass-loader": "16.0.7",
"tsconfig": "workspace:*",
"typescript": "5.9.3",
"vite": "^6.3.5",
"vitest": "0.34.6",
"vite": "^7.3.1",
"vitest": "4.0.18",
"vue-tsc": "^3.0.6"
},
"gitHooks": {
Expand Down
29 changes: 29 additions & 0 deletions apps/container/setup-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,35 @@ class ResizeObserverStub {

window.ResizeObserver = window.ResizeObserver || ResizeObserverStub;

if (!window.visualViewport) {
Object.defineProperty(window, 'visualViewport', {
configurable: true,
value: {
width: window.innerWidth,
height: window.innerHeight,
offsetLeft: 0,
offsetTop: 0,
pageLeft: 0,
pageTop: 0,
scale: 1,
addEventListener: () => undefined,
removeEventListener: () => undefined,
dispatchEvent: () => false,
},
});
}

if (!window.CSS) {
Object.defineProperty(window, 'CSS', {
configurable: true,
value: {
supports: () => false,
},
});
} else if (typeof window.CSS.supports !== 'function') {
window.CSS.supports = () => false;
}

beforeAll(() => server.listen());

afterEach(() => server.resetHandlers());
Expand Down
2 changes: 1 addition & 1 deletion apps/container/src/components/UserMenu/UserMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('<UserMenu />', () => {
beforeEach(() => {
setActivePinia(createPinia());
authStore = useAuthStore();
authStore.authenticate('mock-token');
authStore.token = 'mock-token';
authStore.user = mockedUser;

vi.mocked(useRouter).mockReturnValue({
Expand Down
54 changes: 47 additions & 7 deletions apps/container/src/layouts/AppBar/AppBar.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
import { createPinia, setActivePinia } from 'pinia';
import { useRouter } from 'vue-router';

import { user as mockedUser } from '@/mocks/users';
import { useAuthStore } from '@/stores/auth';
import { render, screen, userEvent, waitFor } from '@/test-utils';

import { AppBar } from '.';

vi.mock('vue-router', async () => ({
useRouter: vi.fn(),
createRouter: vi.fn(() => ({
beforeEach: vi.fn(),
})),
createWebHistory: vi.fn(),
}));

describe('<AppBar />', () => {
let authStore: ReturnType<typeof useAuthStore>;

beforeEach(() => {
setActivePinia(createPinia());
vi.mocked(useRouter).mockReturnValue({
go: vi.fn(),
push: vi.fn(),
replace: vi.fn(),
currentRoute: {
value: {
query: {},
meta: {},
},
},
} as unknown as ReturnType<typeof useRouter>);
authStore = useAuthStore();
authStore.authenticate('mock-token');
authStore.token = 'mock-token';
authStore.user = mockedUser;
});

afterEach(() => {
authStore.logOut();
vi.restoreAllMocks();
});

test('render app bar', () => {
Expand Down Expand Up @@ -55,16 +76,35 @@ describe('<AppBar />', () => {

render(AppBar);

const menuButton = screen.getByRole('button', {
name: 'Expandir menu de usuário',
});
expect(menuButton).toBeInTheDocument();

await user.click(menuButton);
const activator = screen
.getByText(mockedUser.email.replace('@aluno.ufabc.edu.br', ''))
.closest('div');
expect(activator).toBeInTheDocument();
await user.click(activator!);

await waitFor(() => {
expect(screen.getByText('Configurações')).toBeInTheDocument();
expect(screen.getByText('Sair')).toBeInTheDocument();
});
});

test('render without crashing when router injection is unavailable', () => {
vi.mocked(useRouter).mockReturnValue(
undefined as unknown as ReturnType<typeof useRouter>,
);

expect(() =>
render(AppBar, {
global: {
stubs: {
UserMenu: true,
},
},
}),
).not.toThrow();

expect(screen.getAllByRole('img', { name: 'logo do UFABC Next' })).toHaveLength(
2,
);
});
});
6 changes: 3 additions & 3 deletions apps/container/src/layouts/AppBar/AppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,13 @@ import { WebEvent } from '@/helpers/WebEvent';
import { useAuthStore } from '@/stores/auth';
import { applyChartsTheme } from '@/theme';

const router = useRouter();
const router = useRouter() as ReturnType<typeof useRouter> | undefined;
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type annotation as ReturnType<typeof useRouter> | undefined is misleading. In vue-router 5, useRouter() will throw an error if called outside of a Vue component context (no router instance is available), rather than returning undefined. The optional chaining on lines 170, 181 suggests defensive programming, but if useRouter() actually returned undefined in production, it would indicate a serious setup issue. Consider whether this null-safety is necessary or if it's better to let the app fail fast when the router is not properly initialized.

Copilot uses AI. Check for mistakes.
const authStore = useAuthStore();
const theme = useTheme();

const getCurrentDate = () => dayjs();

const layout = computed(() => router.currentRoute.value.meta.layout ?? null);
const layout = computed(() => router?.currentRoute?.value?.meta?.layout ?? null);

const handleLogout = () => {
authStore.logOut();
Expand All @@ -178,7 +178,7 @@ const createAccount = () => {
source: 'app_bar_sidebar',
});

router.push('/signup');
router?.push('/signup');
};

const drawer = ref(false);
Expand Down
4 changes: 3 additions & 1 deletion apps/container/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ const savedTheme = localStorage.getItem('darkMode');
const defaultTheme = savedTheme === 'true' ? 'dark' : 'light';

// Set initial Highcharts theme class
document.body.classList.add(defaultTheme === 'dark' ? 'highcharts-dark' : 'highcharts-light');
document.body.classList.add(
defaultTheme === 'dark' ? 'highcharts-dark' : 'highcharts-light',
);
applyChartsTheme();

const vuetify = createVuetify({
Expand Down
44 changes: 42 additions & 2 deletions apps/container/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,55 @@ import {
} from './stats';
import { user, userGrades } from './users';

const baseUrl = 'https://api.v2.ufabcnext.com';
const baseUrl = 'http://localhost:5000';

export const handlers = [
http.get(`${baseUrl}/users/info`, () => HttpResponse.json(user)),
http.get(`${baseUrl}/entities/enrollments`, () => HttpResponse.json(enrollments)),
http.get(`${baseUrl}/entities/enrollments/*`, () => HttpResponse.json(enrollment)),
http.get(`${baseUrl}/enrollments`, () => HttpResponse.json(enrollments)),
http.get(`${baseUrl}/enrollments/*`, () => HttpResponse.json(enrollment)),
http.get(`${baseUrl}/entities/teachers/reviews/*`, () =>
HttpResponse.json(teacher),
),
http.get(`${baseUrl}/entities/subjects/reviews/*`, () =>
HttpResponse.json(subject),
),
http.get(`${baseUrl}/reviews/teachers/*`, () => HttpResponse.json(teacher)),
http.get(`${baseUrl}/reviews/subjects/*`, () => HttpResponse.json(subject)),
http.get(`${baseUrl}/comments/*`, () => HttpResponse.json(comments)),
http.get(`${baseUrl}/public/stats/usage`, () => HttpResponse.json(usage)),
http.get(`${baseUrl}/stats/usage`, () => HttpResponse.json(usage)),
http.get(`${baseUrl}/public/stats/components/courses`, () =>
HttpResponse.json(courses),
),
http.get(`${baseUrl}/stats/disciplinas/courses`, () =>
HttpResponse.json(courses),
),
http.get(`${baseUrl}/public/stats/components`, ({ request }) => {
const url = new URL(request.url);
if (url.searchParams.get('page') === '1') {
return HttpResponse.json(classesPage1);
}
return HttpResponse.json(classes);
}),
http.get(`${baseUrl}/public/stats/components/component`, ({ request }) => {
const url = new URL(request.url);
if (url.searchParams.get('page') === '1') {
return HttpResponse.json(classesPage1);
}
return HttpResponse.json(classes);
}),
http.get(`${baseUrl}/stats/disciplinas`, ({ request }) => {
const url = new URL(request.url);
if (url.searchParams.get('page') === '1') {
return HttpResponse.json(classesPage1);
}
return HttpResponse.json(classes);
}),
http.get(`${baseUrl}/public/stats/components/overview`, () =>
HttpResponse.json(overview),
),
http.get(`${baseUrl}/stats/disciplinas/overview`, () =>
HttpResponse.json(overview),
),
Expand All @@ -55,19 +84,30 @@ export const handlers = [
http.get(`${baseUrl}/historiesGraduations`, () =>
HttpResponse.json(historiesGraduations),
),
http.get(`${baseUrl}/teachers/search`, () => {
http.get(`${baseUrl}/entities/teachers/search`, () => {
return HttpResponse.json(teacherSearch);
}),
http.get(`${baseUrl}/teachers/search`, () => HttpResponse.json(teacherSearch)),
http.get(`${baseUrl}/entities/subjects/search`, () =>
HttpResponse.json(subjectSearch),
),
http.get(`${baseUrl}/subjects/search`, () =>
HttpResponse.json(subjectSearch),
),
http.delete(`${baseUrl}/users/me/delete`, () => HttpResponse.json({})),
http.delete(`${baseUrl}/users/remove`, () => HttpResponse.json({})),
http.post(`${baseUrl}/account/confirm`, () => HttpResponse.json({})),
http.post(`${baseUrl}/users/confirm`, () => HttpResponse.json({})),
http.post(`${baseUrl}/users/me/recover`, () => HttpResponse.json({})),
http.post(`${baseUrl}/users/recover`, () => HttpResponse.json({})),
http.post(`${baseUrl}/users/me/resend`, () => HttpResponse.json({})),
http.post(`${baseUrl}/users/resend`, () => HttpResponse.json({})),
http.put(`${baseUrl}/users/complete`, () => HttpResponse.json({})),
http.post(`${baseUrl}/comments`, () => HttpResponse.json({})),
http.post(`${baseUrl}/comments/`, () => HttpResponse.json({})),
http.put(`${baseUrl}/comments/*`, () => HttpResponse.json({})),
http.post(`${baseUrl}/comments/reactions/*`, () => HttpResponse.json({})),
http.delete(`${baseUrl}/comments/reactions/*`, () => HttpResponse.json({})),
http.post(`${baseUrl}/reactions/*`, () => HttpResponse.json({})),
http.delete(`${baseUrl}/reactions/*`, () => HttpResponse.json({})),
];
Loading