HR-ATS-Portal/frontend/src/__smoke__/entry.jsx

169 lines
6.0 KiB
JavaScript

/* Test-only entry. Kept inside src/ so every import resolves through Vite's
module graph exactly as it does in the app — one React instance, one router
instance, one query client. Not shipped: excluded from the build because
nothing in the app imports it. */
import React from 'react'
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { MemoryRouter, Navigate, Route, Routes } from 'react-router-dom'
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '../lib/queryClient'
import { initializeCache } from '../data/seedQueries'
import ThemeProvider, { initTheme } from '../theme/ThemeProvider'
import ToastProvider from '../ui/Toast'
import AuthProvider from '../auth/AuthProvider'
import AppLayout from '../app/AppLayout'
import RequireAuth from '../auth/RequireAuth'
import { ROUTES as TABLE } from '../app/routes'
import Login from '../pages/Login'
import Signup from '../pages/Signup'
import ForgotPassword from '../pages/ForgotPassword'
import ConfirmEmail from '../pages/ConfirmEmail'
import Dashboard from '../screens/Dashboard'
import Inbox from '../screens/Inbox'
import Matching from '../screens/Matching'
import Jobs from '../screens/Jobs'
import Candidates from '../screens/Candidates'
import CvBank from '../screens/CvBank'
import Pipeline from '../screens/Pipeline'
import Progress from '../screens/Progress'
import CvImport from '../screens/CvImport'
import JobBoard from '../screens/JobBoard'
import RecruiterHub from '../screens/RecruiterHub'
import Talent from '../screens/Talent'
import Tasks from '../screens/Tasks'
import AiAssistant from '../screens/AiAssistant'
import Interviews from '../screens/Interviews'
import Requisitions from '../screens/Requisitions'
import Assessments from '../screens/Assessments'
import Offers from '../screens/Offers'
import Managers from '../screens/Managers'
import Calendar from '../screens/Calendar'
import Reports from '../screens/Reports'
import Analytics from '../screens/Analytics'
import AiStudio from '../screens/AiStudio'
import Notifications from '../screens/Notifications'
import Rbac from '../screens/Rbac'
import Settings from '../screens/Settings'
import Help from '../screens/Help'
const SCREENS = {
dashboard: Dashboard, inbox: Inbox, matching: Matching, jobs: Jobs, candidates: Candidates,
cvbank: CvBank, pipeline: Pipeline, progress: Progress, import: CvImport, jobboard: JobBoard,
recruiterhub: RecruiterHub, talent: Talent, tasks: Tasks, aiassistant: AiAssistant,
interviews: Interviews, requisitions: Requisitions, assessments: Assessments, offers: Offers,
managers: Managers, calendar: Calendar, reports: Reports, analytics: Analytics,
aistudio: AiStudio, notifications: Notifications, rbac: Rbac,
settings: Settings, help: Help,
}
const PAGES = {
'/auth/login': Login,
'/auth/signup': Signup,
'/auth/forgot-password': ForgotPassword,
'/auth/confirm-email': ConfirmEmail,
}
export const ALL_ROUTES = [
...Object.keys(PAGES),
...TABLE.map((r) => `/${r.path}`),
]
export function boot() {
initTheme()
initializeCache(queryClient)
}
/**
* Like renderRoute, but hands the mount BACK instead of tearing it down.
*
* renderRoute answers "did this route render at all". The Inbox loading test
* asks a different question — what is on screen between one fetch resolving
* and the next — so it needs to step time itself and read the DOM at each
* step. `settle` must come from here, not the test file, because act() has to
* be the bundle's React, not a second copy.
*/
export async function mountRoute(path, container) {
const tree = routeTree(path)
const root = createRoot(container)
await act(async () => { root.render(tree) })
const settle = async (ms = 20) => {
await act(async () => { await new Promise((r) => setTimeout(r, ms)) })
}
await settle()
// Interaction helpers live here for the same reason `settle` does: act() has
// to be the bundle's React instance, not a second copy imported by the test.
const click = async (el) => {
if (!el) throw new Error('click: element not found')
await act(async () => { el.click() })
await settle()
}
const selectOption = async (el, value) => {
if (!el) throw new Error('selectOption: element not found')
const Ev = el.ownerDocument.defaultView.Event
await act(async () => {
el.value = value
el.dispatchEvent(new Ev('change', { bubbles: true }))
})
await settle()
}
return {
settle,
click,
selectOption,
html: () => container.innerHTML,
text: () => container.textContent || '',
find: (selector) => container.querySelector(selector),
findByText: (selector, text) => [...container.querySelectorAll(selector)]
.find((el) => (el.textContent || '').includes(text)) || null,
unmount: async () => { await act(async () => { root.unmount() }) },
}
}
function routeTree(path) {
const h = React.createElement
const isAuth = path.startsWith('/auth/')
const def = TABLE.find((r) => `/${r.path}` === path)
const Screen = isAuth ? PAGES[path] : SCREENS[def.path]
const inner = isAuth
? h(Route, { path, element: h(Screen) })
: h(
Route,
{ element: h(RequireAuth, null, h(AppLayout)) },
h(Route, { path, element: h(Screen) }),
)
return h(
QueryClientProvider, { client: queryClient },
h(ThemeProvider, null,
h(ToastProvider, null,
h(MemoryRouter, { initialEntries: [path] },
h(AuthProvider, null,
h(Routes, null, inner, h(Route, { path: '*', element: h(Navigate, { to: path, replace: true }) })),
),
),
),
),
)
}
/** Mount one route, wait for effects to settle, return its rendered text. */
export async function renderRoute(path, container) {
const tree = routeTree(path)
const root = createRoot(container)
try {
await act(async () => { root.render(tree) })
await act(async () => { await new Promise((r) => setTimeout(r, 40)) })
return container.textContent || ''
} finally {
await act(async () => { root.unmount() })
}
}