Compare commits

...

14 Commits

Author SHA1 Message Date
Allan Carr
0a68d2791d IO-3202 HasFeatureAccess Boolean
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-08 09:51:52 -07:00
Allan Carr
3691d32aaa IO-3202 HasFeatureAccess Boolean
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-07 17:36:39 -07:00
Dave Richer
7e741e4af9 Merged in release/2025-03-28 (pull request #2238)
release/2025-03-28 - Add Cookies Provider
2025-04-02 15:47:49 +00:00
Dave Richer
f556d59ad7 release/2025-03-28 - Add Cookies Provider 2025-04-02 11:38:40 -04:00
Dave Richer
7843ca9b1a Merged in release/2025-03-28 (pull request #2235)
[DO NOT MERGE ]Release/2025-03-28 into master-AIO - IO-2999, IO-3092, IO-3176, IO-3178, IO-3181, IO-3183, IO-3185, IO-3187
2025-04-02 12:51:24 +00:00
Patrick Fic
c8701aba63 Add region capture to Crisp. 2025-04-01 10:03:17 -07:00
Dave Richer
f6e65f82e5 Merged in feature/IO-3181-Test-Framework-Selection (pull request #2233)
Feature/IO-3181 Test Framework Selection
2025-03-28 16:17:31 +00:00
Dave Richer
8b7bb099f3 feature/IO-3181-Test-Framework-Selection - Skeletons complete 2025-03-28 12:16:36 -04:00
Allan Carr
663d91b648 Merged in feature/IO-3187-Admin-Enhancements (pull request #2231)
IO-3187 Admin Enhancements

Approved-by: Dave Richer
2025-03-27 14:04:09 +00:00
Allan Carr
2a7686ec75 IO-3187 Admin Enhancements
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-03-26 15:21:42 -07:00
Dave Richer
549cb56cdf feature/IO-3181-Test-Framework-Selection - Skeletons complete 2025-03-26 16:54:05 -04:00
Dave Richer
146bb6c5c0 feature/IO-3181-Test-Framework-Selection - Skeletons complete 2025-03-26 16:52:59 -04:00
Dave Richer
67b6da7c31 feature/IO-3181-Test-Framework-Selection - Skeletons complete 2025-03-26 16:51:53 -04:00
Allan Carr
624894621b Merged in feature/IO-3176-IntelliPay-Payment-Mapping (pull request #2229)
IO-3176 IntelliPay Payment Mapping

Approved-by: Dave Richer
2025-03-26 18:13:51 +00:00
34 changed files with 3114 additions and 352 deletions

7
.gitignore vendored
View File

@@ -121,3 +121,10 @@ logs/oAuthClient-log.log
/*.env.*
.idea/*
.idea
# Vitest
vitest-report*/
vitest-coverage/
*.vitest.log
test-output.txt

View File

@@ -12,3 +12,5 @@ VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX
TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123"

View File

@@ -14,3 +14,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_COUNTRY=USA
VITE_APP_INSTANCE=ROME
TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123"

11
client/.gitignore vendored
View File

@@ -1,3 +1,14 @@
# Vitest
vitest-report*/
vitest-coverage/
*.vitest.log
test-output.txt
# Playwright
playwright-report/
test-results/
playwright/.cache/
*.playwright.log
# Sentry Config File
.sentryclirc

1468
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,6 @@
"@sentry/vite-plugin": "^3.2.2",
"@splitsoftware/splitio-react": "^2.0.1",
"@tanem/react-nprogress": "^5.0.53",
"@vitejs/plugin-react": "^4.3.4",
"antd": "^5.24.5",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.2.0",
@@ -100,7 +99,14 @@
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"eulaize": "node src/utils/eulaize.js"
"eulaize": "node src/utils/eulaize.js",
"test:unit": "vitest run",
"test:watch": "vitest",
"test:e2e:imex": "playwright test --config playwright.config.js",
"test:e2e:rome": "playwright test --config playwright.rome.config.js",
"test:e2e:imex:headed": "playwright test --config playwright.config.js --headed",
"test:e2e:rome:headed": "playwright test --config playwright.rome.config.js --headed",
"test:e2e:report": "playwright show-report"
},
"browserslist": {
"production": [
@@ -128,7 +134,12 @@
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.23.0",
"@playwright/test": "^1.51.1",
"@sentry/webpack-plugin": "^3.2.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@vitejs/plugin-react": "^4.3.4",
"browserslist": "^4.24.4",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.4.1",
@@ -136,8 +147,10 @@
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-react": "^7.37.4",
"globals": "^15.15.0",
"jsdom": "^26.0.0",
"memfs": "^4.17.0",
"os-browserify": "^0.3.0",
"playwright": "^1.51.1",
"react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
@@ -147,6 +160,7 @@
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.2",
"vite-plugin-style-import": "^2.0.0",
"vitest": "^3.0.9",
"workbox-window": "^7.3.0"
}
}

View File

@@ -0,0 +1,25 @@
import { defineConfig } from "@playwright/test";
import dotenv from "dotenv";
dotenv.config({
path: "./.env.development.imex",
prefix: "TEST_"
});
export default defineConfig({
testDir: "./tests/e2e",
testMatch: "*.e2e.js",
timeout: 60 * 1000,
reporter: [["list"], ["html"]],
use: {
baseURL: "https://localhost:3000",
browser: "chromium",
ignoreHTTPSErrors: true
},
webServer: {
command: "npm run start:imex",
ignoreHTTPSErrors: true,
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
}
});

View File

@@ -0,0 +1,25 @@
import { defineConfig } from "@playwright/test";
import dotenv from "dotenv";
dotenv.config({
path: "./.env.development.rome",
prefix: "TEST_"
});
export default defineConfig({
testDir: "./tests/e2e",
testMatch: "*.e2e.js",
timeout: 60 * 1000,
reporter: [["list"], ["html"]],
use: {
baseURL: "https://localhost:3000",
browser: "chromium",
ignoreHTTPSErrors: true
},
webServer: {
command: "npm run start:rome",
ignoreHTTPSErrors: true,
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
}
});

View File

@@ -10,6 +10,7 @@ import client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
import themeProvider from "./themeProvider";
import { CookiesProvider } from "react-cookie";
// Base Split configuration
const config = {
@@ -38,26 +39,28 @@ function AppContainer() {
const { t } = useTranslation();
return (
<ApolloProvider client={client}>
<ConfigProvider
input={{ autoComplete: "new-password" }}
locale={enLocale}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" })
}
}}
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
<CookiesProvider>
<ApolloProvider client={client}>
<ConfigProvider
input={{ autoComplete: "new-password" }}
locale={enLocale}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" })
}
}}
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
</CookiesProvider>
);
}

View File

@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;

View File

@@ -1,19 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import Alert from "./alert.component";
describe("Alert component", () => {
let wrapper;
beforeEach(() => {
const mockProps = {
type: "error",
message: "Test error message."
};
wrapper = shallow(<Alert {...mockProps} />);
});
it("should render Alert component", () => {
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,31 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import AlertComponent from "./alert.component";
describe("AlertComponent", () => {
it("renders with default props", () => {
render(<AlertComponent message="Default Alert" />);
expect(screen.getByText("Default Alert")).toBeInTheDocument();
expect(screen.getByRole("alert")).toHaveClass("ant-alert");
});
it("applies type prop correctly", () => {
render(<AlertComponent message="Success Alert" type="success" />);
const alert = screen.getByRole("alert");
expect(screen.getByText("Success Alert")).toBeInTheDocument();
expect(alert).toHaveClass("ant-alert-success");
});
it("displays description when provided", () => {
render(<AlertComponent message="Error Alert" description="Something went wrong" type="error" />);
expect(screen.getByText("Error Alert")).toBeInTheDocument();
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
expect(screen.getByRole("alert")).toHaveClass("ant-alert-error");
});
it("is closable and shows icon when props are set", () => {
render(<AlertComponent message="Warning Alert" type="warning" showIcon closable />);
expect(screen.getByText("Warning Alert")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument(); // Close button
});
});

View File

@@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AllocationsAssignmentComponent component should create an allocation on save 1`] = `ReactWrapper {}`;
exports[`AllocationsAssignmentComponent component should render AllocationsAssignmentComponent component 1`] = `ReactWrapper {}`;

View File

@@ -1,35 +0,0 @@
import { mount } from "enzyme";
import React from "react";
import { MockBodyshop } from "../../utils/TestingHelpers";
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
describe("AllocationsAssignmentComponent component", () => {
let wrapper;
beforeEach(() => {
const mockProps = {
bodyshop: MockBodyshop,
handleAssignment: jest.fn(),
assignment: {},
setAssignment: jest.fn(),
visibilityState: [false, jest.fn()],
maxHours: 4
};
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
});
it("should render AllocationsAssignmentComponent component", () => {
expect(wrapper).toMatchSnapshot();
});
it("should render a list of employees", () => {
const empList = wrapper.find("#employeeSelector");
expect(empList.children()).to.have.lengthOf(2);
});
it("should create an allocation on save", () => {
wrapper.find("Button").simulate("click");
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -20,6 +20,7 @@ function FeatureWrapper({
children,
upsellComponent,
bypass,
// eslint-disable-next-line no-unused-vars
...restProps
}) {
const { t } = useTranslation();
@@ -78,7 +79,11 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
}
return true;
}
return bodyshop?.features?.allAccess || dayjs(bodyshop?.features[featureName]).isAfter(dayjs());
return (
bodyshop?.features?.allAccess ||
bodyshop?.features?.[featureName] ||
dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
);
}
export default connect(mapStateToProps, null)(FeatureWrapper);

View File

@@ -340,6 +340,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
args: [],
imex: () => {
window.$crisp.push(["set", "user:company", [payload.shopname]]);
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
if (authRecord[0] && authRecord[0].user.validemail) {
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
}

View File

@@ -1,4 +0,0 @@
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() });

View File

@@ -3679,7 +3679,8 @@
"signinerror": {
"auth/user-disabled": "User account disabled. ",
"auth/user-not-found": "A user with this email does not exist.",
"auth/wrong-password": "The email and password combination you provided is incorrect."
"auth/wrong-password": "The email and password combination you provided is incorrect.",
"auth/invalid-email": "A user with this email does not exist."
}
}
},

View File

@@ -3679,7 +3679,8 @@
"signinerror": {
"auth/user-disabled": "",
"auth/user-not-found": "",
"auth/wrong-password": ""
"auth/wrong-password": "",
"auth/invalid-email": ""
}
}
},

View File

@@ -3679,7 +3679,8 @@
"signinerror": {
"auth/user-disabled": "",
"auth/user-not-found": "",
"auth/wrong-password": ""
"auth/wrong-password": "",
"auth/invalid-email": ""
}
}
},

View File

@@ -1,139 +0,0 @@
export const MockBodyshop = {
address1: "123 Fake St",
address2: "Unit #100",
city: "Vancouver",
country: "Canada",
created_at: "2019-12-10T20:03:06.420853+00:00",
email: "snaptsoft@gmail.com",
federal_tax_id: "GST10150492",
id: "52b7357c-0edd-4c95-85c3-dfdbcdfad9ac",
insurance_vendor_id: "F123456",
logo_img_path: "https://www.snapt.ca/assets/logo-placeholder.png",
md_ro_statuses: {
statuses: [
"Open",
"Scheduled",
"Arrived",
"Repair Plan",
"Parts",
"Body",
"Prep",
"Paint",
"Reassembly",
"Sublet",
"Detail",
"Completed",
"Delivered",
"Invoiced",
"Exported"
],
open_statuses: ["Open", "Scheduled", "Arrived", "Repair Plan", "Parts", "Body", "Prep", "Paint"],
default_arrived: "Arrived",
default_exported: "Exported",
default_imported: "Open",
default_invoiced: "Invoiced",
default_completed: "Completed",
default_delivered: "Delivered",
default_scheduled: "Scheduled"
},
md_order_statuses: {
statuses: ["Ordered", "Received", "Canceled", "Backordered"],
default_bo: "Backordered",
default_ordered: "Ordered",
default_canceled: "Canceled",
default_received: "Received"
},
shopname: "Testing Collision",
state: "BC",
state_tax_id: "PST1000-2991",
updated_at: "2020-03-23T22:06:03.509544+00:00",
zip_post: "V6B 1M9",
region_config: "CA_BC",
md_responsibility_centers: {
costs: [
"Aftermarket",
"ATS",
"Body",
"Detail",
"Daignostic",
"Electrical",
"Chrome",
"Frame",
"Mechanical",
"Refinish",
"Structural",
"Existing",
"Glass",
"LKQ",
"OEM",
"OEM Partial",
"Re-cored",
"Remanufactured",
"Other",
"Sublet",
"Towing"
],
profits: [
"Aftermarket",
"ATS",
"Body",
"Detail",
"Daignostic",
"Electrical",
"Chrome",
"Frame",
"Mechanical",
"Refinish",
"Structural",
"Existing",
"Glass",
"LKQ",
"OEM",
"OEM Partial",
"Re-cored",
"Remanufactured",
"Other",
"Sublet",
"Towing"
],
defaults: {
ATS: "ATS",
LAB: "Body",
LAD: "Diagnostic",
LAE: "Electrical",
LAF: "Frame",
LAG: "Glass",
LAM: "Mechanical",
LAR: "Refinish",
LAS: "Structural",
LAU: "Detail",
PAA: "Aftermarket",
PAC: "Chrome",
PAL: "LKQ",
PAM: "Remanufactured",
PAN: "OEM",
PAO: "Other",
PAP: "OEM Partial",
PAR: "16",
TOW: "Towing"
}
},
employees: [
{
id: "075b744c-8919-49ca-abb2-ccd51040326d",
first_name: "Patrick",
last_name: "BODY123",
employee_number: "101",
cost_center: "Body",
__typename: "employees"
},
{
id: "8cc787d3-1cfe-49d3-8a15-8469cd5c2e41",
first_name: "Patrick",
last_name: "Painter",
employee_number: "10211",
cost_center: "REFINISH",
__typename: "employees"
}
]
};

View File

@@ -0,0 +1,14 @@
// Button.test.jsx
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Button } from "antd";
import "antd/dist/reset.css"; // Optional: include if needed for styling reset
describe("AntD Button", () => {
it("renders with correct text", () => {
render(<Button>Click me</Button>);
const button = screen.getByRole("button", { name: /click me/i });
expect(button).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,6 @@
import { test, expect } from "@playwright/test";
test("homepage loads correctly", async ({ page }) => {
await page.goto("/");
await expect(page.locator("h1")).toContainText("ImEX Online");
});

View File

@@ -0,0 +1,28 @@
import { test, expect } from "@playwright/test";
import { login } from "./utils/login";
test.describe("SignInComponent", () => {
test("successfully logs in with valid credentials", async ({ page }) => {
const email = process.env.TEST_USERNAME;
const password = process.env.TEST_PASSWORD;
await login(page, { email, password });
// Additional assertions after login (optional)
await expect(page).toHaveURL(/\/manage\//);
});
test("displays error on invalid credentials", async ({ page }) => {
await page.goto("/"); // Adjust if login route differs
// Fill form with invalid credentials
await page.fill('input[placeholder="Username"]', "wronguser@example.com");
await page.fill('input[placeholder="Password"]', "wrongpassword");
await page.click("button.login-btn");
// Check for error alert
const alert = page.locator(".ant-alert-error");
await expect(alert).toBeVisible();
await expect(alert).toContainText("A user with this email does not exist.");
});
});

View File

@@ -0,0 +1,21 @@
import { expect } from "@playwright/test";
export async function login(page, { email, password }) {
// Navigate to the login page
await page.goto("/"); // Adjust if your login route differs (e.g., '/login')
// Fill email field
await page.fill('input[placeholder="Username"]', email); // Matches Ant Design Input placeholder
// Fill password field
await page.fill('input[placeholder="Password"]', password);
// Click login button
await page.click("button.login-btn");
// Wait for navigation or success indicator (e.g., redirect to /manage/)
await page.waitForURL(/\/manage\//, { timeout: 10000 }); // Adjust based on redirect
// Verify successful login (e.g., check for a dashboard element)
await expect(page.locator("text=Manage")).toBeVisible(); // Adjust to your apps post-login UI
}

5
client/tests/setup.js Normal file
View File

@@ -0,0 +1,5 @@
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
import "@testing-library/jest-dom";
afterEach(() => cleanup());

27
client/tests/setupI18n.js Normal file
View File

@@ -0,0 +1,27 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en_Translation from "../src/translations/en_us/common.json";
import es_Translation from "../src/translations/es/common.json";
import fr_Translation from "../src/translations/fr/common.json";
const resources = {
"en-US": en_Translation,
"fr-CA": fr_Translation,
"es-MX": es_Translation
};
i18n.use(initReactI18next).init({
resources,
lng: "en-US", // Default to en-US for tests (no LanguageDetector)
fallbackLng: "en-US",
debug: false, // Disable debug in tests
react: {
useSuspense: false // Disable Suspense for Vitest
},
interpolation: {
escapeValue: false, // React handles XSS
skipOnVariables: false
}
});
export default i18n;

19
client/vitest.config.js Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: "./tests/setup.js",
testTimeout: 10000 // 10 seconds (in milliseconds)
},
css: {
preprocessorOptions: {
scss: {
silenceDeprecations: ["import"] // Suppress @import warnings
}
}
}
});

1406
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,9 @@
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
"start": "node server.js",
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\"",
"test:unit": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.772.0",
@@ -82,6 +84,8 @@
"globals": "^15.15.0",
"p-limit": "^3.1.0",
"prettier": "^3.5.3",
"source-map-explorer": "^2.5.2"
"source-map-explorer": "^2.5.2",
"supertest": "^7.1.0",
"vitest": "^3.0.9"
}
}

View File

@@ -39,12 +39,14 @@ exports.createShop = async (req, res) => {
try {
const result = await client.request(
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!){
insert_bodyshops_one(object:$bs){
id
}
}`,
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!) {
insert_bodyshops_one(object: $bs) {
id
vendors {
id
}
}
}`,
{
bs: {
...bodyshop,
@@ -54,12 +56,39 @@ exports.createShop = async (req, res) => {
{ countertype: "ihbnum", count: 1 },
{ countertype: "paymentnum", count: 1 }
]
},
vendors: {
data: [{ name: "In-House" }]
}
}
}
);
res.json(result);
const bodyshopId = result.insert_bodyshops_one.id;
const vendorId = result.insert_bodyshops_one.vendors[0].id;
if (!bodyshopId || !vendorId) {
throw new Error("Failed to create bodyshop or vendor");
}
const updateBodyshop = await client.request(
`mutation UPDATE_BODYSHOP($id: uuid!, $inhousevendorid: uuid!) {
update_bodyshops_by_pk(pk_columns: { id: $id }, _set: { inhousevendorid: $inhousevendorid }) {
id
}
}`,
{
id: bodyshopId,
inhousevendorid: vendorId
}
);
res.status(200).json(updateBodyshop);
} catch (error) {
logger.log("admin-create-shop-error", "error", req.user.email, null, {
message: error.message,
stack: error.stack,
request: req.body,
ioadmin: true
});
res.status(500).json(error);
}
};

14
server/tests/api.test.js Normal file
View File

@@ -0,0 +1,14 @@
import { describe, it, expect } from "vitest";
import request from "supertest";
import express from "express";
const app = express();
app.get("/api/health", (req, res) => res.json({ status: "ok" }));
describe("API", () => {
it("returns health status", async () => {
const response = await request(app).get("/api/health");
expect(response.status).toBe(200);
expect(response.body).toEqual({ status: "ok" });
});
});

11
server/tests/math.test.js Normal file
View File

@@ -0,0 +1,11 @@
import { describe, it, expect } from "vitest";
function add(a, b) {
return a + b;
}
describe("Math", () => {
it("adds two numbers correctly", () => {
expect(add(2, 3)).toBe(5);
});
});

10
vitest.config.js Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
globals: true,
include: ["./server/tests/**/*.{test,spec}.[jt]s"], // Only search /tests in root
exclude: ["**/client/**", "**/node_modules/**", "**/dist/**"] // Explicitly exclude /client
}
});