Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/dmaman86/shiftly/llms.txt

Use this file to discover all available pages before exploring further.

Shiftly’s test suite is built on Vitest and the Testing Library family. Tests are split into two broad concerns: the pure domain layer — which implements all Israeli labor law pay rules independently of any framework — and the UI layer — which validates React components, hooks, layouts, providers, and routes. Because the domain layer is framework-agnostic, its tests run without any React or browser environment setup, making them fast and deterministic.

Testing Stack

PackageRole
vitestTest runner, assertion library, and coverage orchestrator
@testing-library/reactRender React components in a jsdom environment
@testing-library/jest-domExtends Vitest expect with DOM matchers (.toBeInTheDocument(), etc.)
@testing-library/user-eventSimulates realistic browser interactions (typing, clicking, etc.)
jsdomProvides a DOM environment for all tests
The global test setup lives in src/test/setup.ts. It extends Vitest’s expect with jest-dom matchers and registers an afterEach cleanup hook that unmounts React trees between tests:
import { expect, afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
import * as matchers from "@testing-library/jest-dom/matchers";
import "@/i18n";

expect.extend(matchers);

afterEach(() => {
  cleanup();
});

Test Directory Structure

All tests live under src/test/ and mirror the structure of the source code they cover:
src/test/
├── domain/
│   ├── builder/     # ShiftMapBuilder, DayPayMapBuilder, WorkDaysForMonthBuilder
│   ├── calculator/  # Regular, Extra, Special, PerDiem, MealAllowance calculators
│   ├── reducer/     # All five pay-map reducers
│   ├── resolve/     # All resolvers (Holiday, WorkDay, Month, PerDiem, MealAllowance)
│   ├── services/    # DateService, ShiftService
│   └── e2e/         # Full salary pipeline end-to-end tests
├── ui/
│   ├── components/  # Individual component rendering and interaction tests
│   ├── hooks/       # Hook behaviour tests
│   ├── layout/      # Layout component tests
│   ├── providers/   # Provider integration tests
│   └── routes/      # Route-level rendering tests
├── adapters/        # Adapter layer tests
└── services/        # Analytics and gtag service tests

Running Tests

bun run test
Running test:ui opens the Vitest browser-based test explorer. It lets you browse the test file tree, run individual suites or cases with a single click, inspect the call graph, and view live pass/fail status — useful during active development of new test cases.

Test Utilities

Three helper modules under src/test/ui/utils/ provide the scaffolding for UI tests.

test-utils.tsx — Custom render with all providers

Rather than importing render directly from @testing-library/react, UI tests use renderWithProviders from test-utils.tsx. This wraps any component under test with the full provider stack: Redux Provider, BrowserRouter, MUI ThemeProvider, LocalizationProvider, and SnackbarProvider.
export function renderWithProviders(
  ui: ReactElement,
  {
    preloadedState,
    store = createMockStore(preloadedState),
    withRouter = true,
    withTheme = true,
    withSnackbar = true,
    ...renderOptions
  }: ExtendedRenderOptions = {}
) { ... }
Each provider wrapper is opt-out via flags (withRouter, withTheme, withSnackbar), so a test for a purely presentational component can skip the router or snackbar without ceremony. A createMockStore helper creates a fresh Redux store per test, optionally seeded with preloadedState:
export function createMockStore(preloadedState?: Partial<RootState>) {
  return configureStore({
    reducer: {
      workDays: workDaysReducer,
      global: globalReducer,
    },
    preloadedState: preloadedState as RootState,
  });
}
The module also re-exports screen, within, waitFor, waitForElementToBeRemoved, fireEvent, act, cleanup, and userEvent so test files have a single import point.

mock-factories.ts — Factory functions for test data

mock-factories.ts exports typed factory functions that produce valid Redux state objects with sensible defaults. Tests can override individual fields without constructing the full shape manually:
export function createMockGlobalState(
  overrides?: Partial<GlobalState>
): GlobalState {
  return {
    config: {
      standardHours: 6.67,
      baseRate: 50,
      year: 2024,
      month: 1,
      ...overrides?.config,
    },
    dailyPayMaps: overrides?.dailyPayMaps ?? {},
    globalBreakdown:
      overrides?.globalBreakdown ??
      payMap.monthPayMapCalculator.createEmpty(),
  };
}

export function createMockWorkDaysState(
  overrides?: Partial<WorkDaysState>
): WorkDaysState {
  return {
    year: overrides?.year ?? 2024,
    month: overrides?.month ?? 1,
    workDays: overrides?.workDays ?? [],
  };
}

setup-domain.ts — Domain instance setup for UI tests

UI tests that depend on Redux slices which call into the domain pipeline need a consistent domain mock. setup-domain.ts builds a single PayMapPipeline instance once via buildPayMapPipeline(), then uses vi.mock inside a beforeAll hook to replace the @/app module with that instance before any test in the suite runs:
import { vi, beforeAll } from "vitest";
import { buildPayMapPipeline } from "@/domain";

const pipelineInstance = buildPayMapPipeline();

beforeAll(() => {
  vi.mock("@/app", () => ({
    domain: {
      payMap: {
        shiftMapBuilder: pipelineInstance.payMap.shiftMapBuilder,
        dayPayMapBuilder: pipelineInstance.payMap.dayPayMapBuilder,
        monthPayMapCalculator: pipelineInstance.payMap.monthPayMapCalculator,
        workDaysMonthBuilder: pipelineInstance.payMap.workDaysForMonthBuilder,
      },
      resolvers: { ... },
      services: { ... },
    },
  }));
});

export { pipelineInstance };
mock-factories.ts imports pipelineInstance from this module so factory-generated state objects use the same pipeline instance as the mocked Redux slices.

Domain Layer Testing

The domain layer is entirely framework-agnostic. Calculators, builders, reducers, resolvers, and services have no React or Redux dependencies, so they are imported and called directly in tests — no providers, no stores, no rendering. This makes domain tests the fastest and most reliable part of the suite.

Key principle

All pay-rule logic lives in plain TypeScript classes and functions. Tests import buildPayMapPipeline() directly, call its methods, and assert on the returned data structures — exactly the same way the Redux slices consume the domain at runtime.

The End-to-End Salary Pipeline Test

src/test/domain/e2e/salary-pipeline.e2e.test.ts is the most comprehensive test file in the codebase. It validates the complete calculation flow from a raw Shift input through to the monthly aggregate structure by exercising the pipeline at every layer. The test suite constructs a fresh PayMapPipeline before each test using buildPayMapPipeline() and covers the following scenarios: Pipeline component availability — verifies that every builder, resolver, and service is initialised and that each call to buildPayMapPipeline() produces a distinct instance. Shift layer — individual shift processing:
  • A standard 8-hour regular shift produces 8 hours at 100%, 0 at 125%, 0 at 150%.
  • A 10-hour shift splits into 8 hours at 100% and 2 hours at 125% (first overtime tier).
  • A 12-hour shift produces the full 8 / 2 / 2 split across all three rate tiers.
  • Night shifts crossing midnight (e.g., 22:00 – 06:00) calculate the correct total duration.
  • Duty shifts (isDuty: true) populate perDiemShift with isFieldDutyShift: true and the correct hours count.
  • Shabbat shifts (Saturday, WorkDayType.SpecialFull) produce populated special hour buckets.
Day layer — monthly structure:
  • workDaysForMonthBuilder.build({ year, month, eventMap }) returns the correct number of days for January (31), February in a leap year (29), and February in a non-leap year (28).
  • All Saturdays in the month are classified as WorkDayType.SpecialFull.
  • Friday days are correctly identified by their Hebrew day label ("ו").
Month layer — aggregate structure:
  • monthPayMapCalculator.createEmpty() returns a MonthPayMap with all numeric fields zeroed.
  • The full structure includes regular, extra, special, hours100Sick, hours100Vacation, extra100Shabbat, perDiem, mealAllowance, and totalHours.
Complete pipeline flow:
  • A single shift processed end-to-end (build month → find work day → build shift map) produces the expected hour breakdown.
  • A realistic work week (Mon–Thu regular + Thursday overtime) yields correct per-day and aggregate totals.
  • A mixed month scenario (regular days, overtime, night shifts) processes without errors.
Edge cases:
  • Year-boundary shifts (Dec 31 → Jan 1) calculate the correct 8-hour total.
  • Shifts on February 29 in a leap year are handled correctly.
  • Month lengths of 30 and 31 days are verified at both ends of the date range.
Performance: the full month (31 days, one shift per day) processes in under 100 ms.

CI Pipeline

The GitHub Actions workflow at .github/workflows/ci.yml runs on every push to main or master. It provisions a Bun environment, installs dependencies with a frozen lockfile, and then executes the quality gate sequence:
- name: Run type check
  run: bun run typecheck

- name: Run linter
  run: bun run lint

- name: Run tests with coverage
  run: bun run test:coverage

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v4
  with:
    files: ./coverage/coverage-final.json
    flags: unittests

- name: Build application
  run: bun run build
Coverage results are uploaded to Codecov using the coverage-final.json report generated by Vitest’s V8 provider. The build step runs last so that a failing test or type error blocks the build artifact from being produced.

Build docs developers (and LLMs) love