feature/IO-3587-Commision-Cut - Additional Testing / Test Harness improvements
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { Button } from "antd";
|
||||
import { Button, Card, Divider, Form, Space, Typography } from "antd";
|
||||
import { connect } from "react-redux";
|
||||
import queryString from "query-string";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { PayrollLaborAllocationsTable } from "../labor-allocations-table/labor-allocations-table.payroll.component.jsx";
|
||||
import { TimeTicketTaskModalComponent } from "../time-ticket-task-modal/time-ticket-task-modal.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
@@ -9,8 +13,109 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
setRefundPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "refund_payment" }))
|
||||
});
|
||||
|
||||
const commissionCutFixture = {
|
||||
bodyshop: {
|
||||
features: {
|
||||
timetickets: true
|
||||
},
|
||||
employees: [
|
||||
{ id: "emp-1", first_name: "Avery", last_name: "Johnson" },
|
||||
{ id: "emp-2", first_name: "Morgan", last_name: "Lee" }
|
||||
],
|
||||
md_tasks_presets: {
|
||||
presets: [
|
||||
{
|
||||
name: "Body Prep",
|
||||
percent: 50,
|
||||
hourstype: ["LAA", "LAB"],
|
||||
nextstatus: "In Progress"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
jobId: "fixture-job-1",
|
||||
joblines: [
|
||||
{
|
||||
id: "line-1",
|
||||
mod_lbr_ty: "LAA",
|
||||
mod_lb_hrs: 4,
|
||||
assigned_team: "team-1",
|
||||
convertedtolbr: false
|
||||
}
|
||||
],
|
||||
previewValues: {
|
||||
task: "Body Prep",
|
||||
timetickets: [
|
||||
{
|
||||
employeeid: "emp-1",
|
||||
cost_center: "Body",
|
||||
ciecacode: "LAA",
|
||||
productivehrs: 2,
|
||||
rate: 40,
|
||||
payoutamount: 80,
|
||||
payout_context: {
|
||||
payout_method: "commission"
|
||||
}
|
||||
},
|
||||
{
|
||||
employeeid: "emp-2",
|
||||
cost_center: "Refinish",
|
||||
ciecacode: "LAB",
|
||||
productivehrs: 1,
|
||||
rate: 28,
|
||||
payoutamount: 28,
|
||||
payout_context: {
|
||||
payout_method: "hourly"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function CommissionCutHarness() {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Typography.Title level={2}>Commission Cut Test Harness</Typography.Title>
|
||||
<Typography.Paragraph>
|
||||
This fixture keeps commission-cut browser checks stable by rendering representative payroll and preview UI with
|
||||
local data.
|
||||
</Typography.Paragraph>
|
||||
<Card title="Payroll Labor Allocations">
|
||||
<PayrollLaborAllocationsTable
|
||||
jobId={commissionCutFixture.jobId}
|
||||
joblines={commissionCutFixture.joblines}
|
||||
timetickets={[]}
|
||||
bodyshop={commissionCutFixture.bodyshop}
|
||||
adjustments={[]}
|
||||
refetch={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
<Divider />
|
||||
<Card title="Claim Task Preview">
|
||||
<Form form={form} initialValues={commissionCutFixture.previewValues} layout="vertical">
|
||||
<TimeTicketTaskModalComponent
|
||||
bodyshop={commissionCutFixture.bodyshop}
|
||||
form={form}
|
||||
loading={false}
|
||||
completedTasks={[]}
|
||||
unassignedHours={1.25}
|
||||
/>
|
||||
</Form>
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
function Test({ setRefundPaymentContext, refundPaymentModal }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
console.log("refundPaymentModal", refundPaymentModal);
|
||||
|
||||
if (search.fixture === "commission-cut") {
|
||||
return <CommissionCutHarness />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { LaborAllocationsAdjustmentEdit } from "./labor-allocations-adjustment-edit.component.jsx";
|
||||
|
||||
const updateAdjustmentsMock = vi.fn();
|
||||
const useMutationMock = vi.fn();
|
||||
const notification = {
|
||||
success: vi.fn(),
|
||||
error: vi.fn()
|
||||
};
|
||||
const insertAuditTrailMock = vi.fn();
|
||||
const jobmodifylbradjMock = vi.fn(() => "audit-entry");
|
||||
|
||||
vi.mock("@apollo/client/react", () => ({
|
||||
useMutation: (...args) => useMutationMock(...args)
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key) => {
|
||||
const translations = {
|
||||
"joblines.fields.mod_lbr_ty": "Labor Type",
|
||||
"joblines.fields.lbr_types.LAA": "LAA",
|
||||
"jobs.fields.adjustmenthours": "Adjustment Hours",
|
||||
"general.actions.save": "Save",
|
||||
"jobs.successes.save": "Saved"
|
||||
};
|
||||
|
||||
return translations[key] || key;
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock("../../contexts/Notifications/notificationContext.jsx", () => ({
|
||||
useNotification: () => notification
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/AuditTrailMappings", () => ({
|
||||
default: {
|
||||
jobmodifylbradj: (...args) => jobmodifylbradjMock(...args)
|
||||
}
|
||||
}));
|
||||
|
||||
describe("LaborAllocationsAdjustmentEdit", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
useMutationMock.mockImplementation((mutation) => {
|
||||
if (mutation === UPDATE_JOB) {
|
||||
return [updateAdjustmentsMock];
|
||||
}
|
||||
|
||||
return [vi.fn()];
|
||||
});
|
||||
|
||||
updateAdjustmentsMock.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("saves merged labor adjustments and records the adjustment delta", async () => {
|
||||
render(
|
||||
<LaborAllocationsAdjustmentEdit
|
||||
insertAuditTrail={insertAuditTrailMock}
|
||||
jobId="job-1"
|
||||
mod_lbr_ty="LAA"
|
||||
adjustments={{
|
||||
LAA: 1.2,
|
||||
LAB: 0.5
|
||||
}}
|
||||
refetchQueryNames={["QUERY_JOB"]}
|
||||
>
|
||||
<button type="button">Edit Adjustment</button>
|
||||
</LaborAllocationsAdjustmentEdit>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Edit Adjustment" }));
|
||||
|
||||
fireEvent.change(screen.getByRole("spinbutton"), {
|
||||
target: { value: "3.7" }
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateAdjustmentsMock).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
jobId: "job-1",
|
||||
job: {
|
||||
lbr_adjustments: {
|
||||
LAA: 3.7,
|
||||
LAB: 0.5
|
||||
}
|
||||
}
|
||||
},
|
||||
refetchQueries: ["QUERY_JOB"]
|
||||
});
|
||||
});
|
||||
|
||||
expect(jobmodifylbradjMock).toHaveBeenCalledWith({
|
||||
mod_lbr_ty: "LAA",
|
||||
hours: 2.5
|
||||
});
|
||||
expect(insertAuditTrailMock).toHaveBeenCalledWith({
|
||||
jobid: "job-1",
|
||||
operation: "audit-entry",
|
||||
type: "jobmodifylbradj"
|
||||
});
|
||||
expect(notification.success).toHaveBeenCalledWith({
|
||||
title: "Saved"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import axios from "axios";
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
import { PayrollLaborAllocationsTable } from "./labor-allocations-table.payroll.component.jsx";
|
||||
|
||||
const notification = {
|
||||
success: vi.fn(),
|
||||
error: vi.fn()
|
||||
};
|
||||
|
||||
vi.mock("axios", () => ({
|
||||
default: {
|
||||
post: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key, values = {}) => {
|
||||
const translations = {
|
||||
"jobs.labels.laborallocations": "Labor Allocations",
|
||||
"timetickets.actions.payall": "Pay All",
|
||||
"general.labels.totals": "Totals",
|
||||
"jobs.labels.outstandinghours": "Outstanding hours remain."
|
||||
};
|
||||
|
||||
if (key === "timetickets.successes.payall") {
|
||||
return "All hours paid out successfully.";
|
||||
}
|
||||
|
||||
if (key === "timetickets.errors.payall") {
|
||||
return `Error flagging hours. ${values.error || ""}`.trim();
|
||||
}
|
||||
|
||||
return translations[key] || key;
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock("../../contexts/Notifications/notificationContext.jsx", () => ({
|
||||
useNotification: () => notification
|
||||
}));
|
||||
|
||||
vi.mock("../responsive-table/responsive-table.component", () => {
|
||||
function ResponsiveTable({ dataSource = [], summary }) {
|
||||
return (
|
||||
<div data-testid="responsive-table">
|
||||
<div>{`rows:${dataSource.length}`}</div>
|
||||
{summary ? <div>{summary()}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ResponsiveTable.Summary = {
|
||||
Row: ({ children }) => <div>{children}</div>,
|
||||
Cell: ({ children }) => <div>{children}</div>
|
||||
};
|
||||
|
||||
return {
|
||||
default: ResponsiveTable
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../feature-wrapper/feature-wrapper.component", () => ({
|
||||
HasFeatureAccess: () => true
|
||||
}));
|
||||
|
||||
vi.mock("../upsell/upsell.component", () => ({
|
||||
default: () => <div>Upsell</div>,
|
||||
upsellEnum: () => ({
|
||||
timetickets: {
|
||||
allocations: "allocations"
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock("../lock-wrapper/lock-wrapper.component", () => ({
|
||||
default: ({ children }) => children
|
||||
}));
|
||||
|
||||
describe("PayrollLaborAllocationsTable", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows a success notification after Pay All completes", async () => {
|
||||
axios.post
|
||||
.mockResolvedValueOnce({
|
||||
data: [
|
||||
{
|
||||
employeeid: "emp-1",
|
||||
mod_lbr_ty: "LAA",
|
||||
expectedHours: 4,
|
||||
claimedHours: 1
|
||||
}
|
||||
]
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
data: [{ id: "ticket-1" }]
|
||||
});
|
||||
|
||||
const refetch = vi.fn();
|
||||
|
||||
render(
|
||||
<PayrollLaborAllocationsTable
|
||||
jobId="job-1"
|
||||
joblines={[{ id: "line-1", convertedtolbr: false }]}
|
||||
timetickets={[]}
|
||||
bodyshop={{
|
||||
features: {
|
||||
timetickets: true
|
||||
},
|
||||
employees: [{ id: "emp-1", first_name: "Avery", last_name: "Johnson" }]
|
||||
}}
|
||||
adjustments={[]}
|
||||
refetch={refetch}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axios.post).toHaveBeenNthCalledWith(1, "/payroll/calculatelabor", {
|
||||
jobid: "job-1"
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Pay All" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axios.post).toHaveBeenNthCalledWith(2, "/payroll/payall", {
|
||||
jobid: "job-1"
|
||||
});
|
||||
});
|
||||
|
||||
expect(notification.success).toHaveBeenCalledWith({
|
||||
title: "All hours paid out successfully."
|
||||
});
|
||||
expect(refetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows the returned pay-all error message when payroll rejects the request", async () => {
|
||||
axios.post
|
||||
.mockResolvedValueOnce({
|
||||
data: [
|
||||
{
|
||||
employeeid: "emp-1",
|
||||
mod_lbr_ty: "LAA",
|
||||
expectedHours: 4,
|
||||
claimedHours: 1
|
||||
}
|
||||
]
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
data: {
|
||||
success: false,
|
||||
error: "Not all hours have been assigned."
|
||||
}
|
||||
});
|
||||
|
||||
render(
|
||||
<PayrollLaborAllocationsTable
|
||||
jobId="job-1"
|
||||
joblines={[{ id: "line-1", convertedtolbr: false }]}
|
||||
timetickets={[]}
|
||||
bodyshop={{
|
||||
features: {
|
||||
timetickets: true
|
||||
},
|
||||
employees: [{ id: "emp-1", first_name: "Avery", last_name: "Johnson" }]
|
||||
}}
|
||||
adjustments={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axios.post).toHaveBeenNthCalledWith(1, "/payroll/calculatelabor", {
|
||||
jobid: "job-1"
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Pay All" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(notification.error).toHaveBeenCalledWith({
|
||||
title: "Error flagging hours. Not all hours have been assigned."
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -36,14 +36,20 @@ import {
|
||||
} from "../../graphql/employee_teams.queries";
|
||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import {
|
||||
LABOR_TYPES,
|
||||
getSplitTotal,
|
||||
hasExactSplitTotal,
|
||||
normalizeEmployeeTeam,
|
||||
normalizeTeamMember,
|
||||
validateEmployeeTeamMembers
|
||||
} from "./shop-employee-teams.form.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
const LABOR_TYPES = ["LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU", "LA1", "LA2", "LA3", "LA4"];
|
||||
|
||||
const PAYOUT_METHOD_OPTIONS = [
|
||||
{ labelKey: "employee_teams.options.hourly", value: "hourly" },
|
||||
{ labelKey: "employee_teams.options.commission_percentage", value: "commission" }
|
||||
@@ -57,23 +63,6 @@ const TEAM_MEMBER_PRIMARY_FIELD_COLS = {
|
||||
|
||||
const TEAM_MEMBER_RATE_FIELD_COLS = { xs: 24, sm: 12, md: 8, lg: 6, xxl: 4 };
|
||||
|
||||
const normalizeTeamMember = (teamMember = {}) => ({
|
||||
...teamMember,
|
||||
payout_method: teamMember.payout_method || "hourly",
|
||||
labor_rates: teamMember.labor_rates || {},
|
||||
commission_rates: teamMember.commission_rates || {}
|
||||
});
|
||||
|
||||
const normalizeEmployeeTeam = (employeeTeam = {}) => ({
|
||||
...employeeTeam,
|
||||
employee_team_members: (employeeTeam.employee_team_members || []).map(normalizeTeamMember)
|
||||
});
|
||||
|
||||
const getSplitTotal = (teamMembers = []) =>
|
||||
teamMembers.reduce((sum, member) => sum + Number(member?.percentage || 0), 0);
|
||||
|
||||
const hasExactSplitTotal = (teamMembers = []) => Math.abs(getSplitTotal(teamMembers) - 100) < 0.00001;
|
||||
|
||||
const getPayoutMethodTagColor = (payoutMethod) => (payoutMethod === "commission" ? "gold" : "blue");
|
||||
|
||||
const getEmployeeDisplayName = (employees = [], employeeId) => {
|
||||
@@ -170,32 +159,11 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
};
|
||||
|
||||
const handleFinish = async ({ employee_team_members = [], ...values }) => {
|
||||
const normalizedTeamMembers = employee_team_members.map((teamMember) => {
|
||||
const nextTeamMember = normalizeTeamMember({ ...teamMember });
|
||||
delete nextTeamMember.__typename;
|
||||
return nextTeamMember;
|
||||
});
|
||||
const { normalizedTeamMembers, errorKey } = validateEmployeeTeamMembers(employee_team_members);
|
||||
|
||||
if (normalizedTeamMembers.length === 0) {
|
||||
if (errorKey) {
|
||||
notification.error({
|
||||
title: t("employee_teams.errors.minimum_one_member")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const employeeIds = normalizedTeamMembers.map((teamMember) => teamMember.employeeid).filter(Boolean);
|
||||
const duplicateEmployeeIds = employeeIds.filter((employeeId, index) => employeeIds.indexOf(employeeId) !== index);
|
||||
|
||||
if (duplicateEmployeeIds.length > 0) {
|
||||
notification.error({
|
||||
title: t("employee_teams.errors.duplicate_member")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasExactSplitTotal(normalizedTeamMembers)) {
|
||||
notification.error({
|
||||
title: t("employee_teams.errors.allocation_total_exact")
|
||||
title: t(errorKey)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { INSERT_EMPLOYEE_TEAM, UPDATE_EMPLOYEE_TEAM } from "../../graphql/employee_teams.queries";
|
||||
import { LABOR_TYPES } from "./shop-employee-teams.form.utils.js";
|
||||
import { ShopEmployeeTeamsFormComponent } from "./shop-employee-teams.form.component.jsx";
|
||||
|
||||
const insertEmployeeTeamMock = vi.fn();
|
||||
const updateEmployeeTeamMock = vi.fn();
|
||||
const useQueryMock = vi.fn();
|
||||
const useMutationMock = vi.fn();
|
||||
const navigateMock = vi.fn();
|
||||
const notification = {
|
||||
error: vi.fn(),
|
||||
success: vi.fn()
|
||||
};
|
||||
|
||||
vi.mock("@apollo/client/react", () => ({
|
||||
useQuery: (...args) => useQueryMock(...args),
|
||||
useMutation: (...args) => useMutationMock(...args)
|
||||
}));
|
||||
|
||||
vi.mock("react-router-dom", () => ({
|
||||
useLocation: () => ({
|
||||
search: "?employeeTeamId=new"
|
||||
}),
|
||||
useNavigate: () => navigateMock
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key, values = {}) => {
|
||||
const translations = {
|
||||
"employee_teams.fields.name": "Team Name",
|
||||
"employee_teams.fields.active": "Active",
|
||||
"employee_teams.fields.max_load": "Max Load",
|
||||
"employee_teams.fields.employeeid": "Employee",
|
||||
"employee_teams.fields.allocation_percentage": "Allocation %",
|
||||
"employee_teams.fields.payout_method": "Payout Method",
|
||||
"employee_teams.fields.allocation": "Allocation",
|
||||
"employee_teams.fields.employeeid_label": "Employee",
|
||||
"employee_teams.options.hourly": "Hourly",
|
||||
"employee_teams.options.commission": "Commission",
|
||||
"employee_teams.options.commission_percentage": "Commission",
|
||||
"employee_teams.actions.newmember": "New Team Member",
|
||||
"employee_teams.errors.minimum_one_member": "Add at least one team member.",
|
||||
"employee_teams.errors.duplicate_member": "Team members must be unique.",
|
||||
"employee_teams.errors.allocation_total_exact": "Allocation must total exactly 100%.",
|
||||
"general.actions.save": "Save",
|
||||
"employees.successes.save": "Saved"
|
||||
};
|
||||
|
||||
if (key === "employee_teams.labels.allocation_total") {
|
||||
return `Allocation Total: ${values.total}%`;
|
||||
}
|
||||
|
||||
if (key.startsWith("joblines.fields.lbr_types.")) {
|
||||
return key.split(".").pop();
|
||||
}
|
||||
|
||||
return translations[key] || key;
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock("../../contexts/Notifications/notificationContext.jsx", () => ({
|
||||
useNotification: () => notification
|
||||
}));
|
||||
|
||||
vi.mock("../../firebase/firebase.utils", () => ({
|
||||
logImEXEvent: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock("../employee-search-select/employee-search-select.component", () => ({
|
||||
default: ({ id, value, onChange, options = [] }) => (
|
||||
<select
|
||||
aria-label="Employee"
|
||||
id={id}
|
||||
value={value ?? ""}
|
||||
onChange={(event) => onChange?.(event.target.value || undefined)}
|
||||
>
|
||||
<option value="">Select Employee</option>
|
||||
{options.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{[option.first_name, option.last_name].filter(Boolean).join(" ")}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}));
|
||||
|
||||
vi.mock("../form-items-formatted/currency-form-item.component", () => ({
|
||||
default: ({ id, value, onChange }) => (
|
||||
<input
|
||||
data-testid="currency-input"
|
||||
id={id}
|
||||
type="text"
|
||||
value={value ?? ""}
|
||||
onChange={(event) => onChange?.(event.target.value === "" ? null : Number(event.target.value))}
|
||||
/>
|
||||
)
|
||||
}));
|
||||
|
||||
vi.mock("../layout-form-row/layout-form-row.component", () => ({
|
||||
default: ({ title, extra, children }) => (
|
||||
<div>
|
||||
{title}
|
||||
{extra}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}));
|
||||
|
||||
vi.mock("../form-list-move-arrows/form-list-move-arrows.component", () => ({
|
||||
default: () => null
|
||||
}));
|
||||
|
||||
const bodyshop = {
|
||||
id: "shop-1",
|
||||
employees: [
|
||||
{
|
||||
id: "emp-1",
|
||||
first_name: "Avery",
|
||||
last_name: "Johnson"
|
||||
},
|
||||
{
|
||||
id: "emp-2",
|
||||
first_name: "Morgan",
|
||||
last_name: "Lee"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const fillHourlyRates = (value) => {
|
||||
LABOR_TYPES.forEach((laborType) => {
|
||||
fireEvent.change(screen.getByLabelText(laborType), {
|
||||
target: { value: String(value) }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addBaseTeamMember = ({ employeeId = "emp-1", percentage = 100, rate = 25 } = {}) => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "New Team Member" }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Employee"), {
|
||||
target: { value: employeeId }
|
||||
});
|
||||
fireEvent.change(screen.getByRole("spinbutton", { name: "Allocation %" }), {
|
||||
target: { value: String(percentage) }
|
||||
});
|
||||
fillHourlyRates(rate);
|
||||
};
|
||||
|
||||
describe("ShopEmployeeTeamsFormComponent", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
useQueryMock.mockReturnValue({
|
||||
error: null,
|
||||
data: null,
|
||||
loading: false
|
||||
});
|
||||
|
||||
useMutationMock.mockImplementation((mutation) => {
|
||||
if (mutation === UPDATE_EMPLOYEE_TEAM) {
|
||||
return [updateEmployeeTeamMock];
|
||||
}
|
||||
|
||||
if (mutation === INSERT_EMPLOYEE_TEAM) {
|
||||
return [insertEmployeeTeamMock];
|
||||
}
|
||||
|
||||
return [vi.fn()];
|
||||
});
|
||||
|
||||
insertEmployeeTeamMock.mockResolvedValue({
|
||||
data: {
|
||||
insert_employee_teams_one: {
|
||||
id: "team-1"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("switches a new team member from hourly rates to commission percentages", async () => {
|
||||
render(<ShopEmployeeTeamsFormComponent bodyshop={bodyshop} />);
|
||||
|
||||
addBaseTeamMember();
|
||||
expect(screen.getAllByTestId("currency-input")).toHaveLength(LABOR_TYPES.length);
|
||||
|
||||
fireEvent.mouseDown(screen.getByRole("combobox", { name: "Payout Method" }));
|
||||
fireEvent.click(screen.getByText("Commission"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId("currency-input")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("submits a valid new hourly team with normalized member data", async () => {
|
||||
render(<ShopEmployeeTeamsFormComponent bodyshop={bodyshop} />);
|
||||
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Team Name" }), {
|
||||
target: { value: "Commission Crew" }
|
||||
});
|
||||
fireEvent.change(screen.getByRole("spinbutton", { name: "Max Load" }), {
|
||||
target: { value: "8" }
|
||||
});
|
||||
|
||||
addBaseTeamMember({
|
||||
employeeId: "emp-1",
|
||||
percentage: 100,
|
||||
rate: 27.5
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(insertEmployeeTeamMock).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
employeeTeam: {
|
||||
name: "Commission Crew",
|
||||
max_load: 8,
|
||||
employee_team_members: {
|
||||
data: [
|
||||
{
|
||||
employeeid: "emp-1",
|
||||
percentage: 100,
|
||||
payout_method: "hourly",
|
||||
labor_rates: Object.fromEntries(LABOR_TYPES.map((laborType) => [laborType, 27.5])),
|
||||
commission_rates: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
bodyshopid: "shop-1"
|
||||
}
|
||||
},
|
||||
refetchQueries: ["QUERY_TEAMS"]
|
||||
});
|
||||
});
|
||||
|
||||
expect(notification.success).toHaveBeenCalledWith({
|
||||
title: "Saved"
|
||||
});
|
||||
expect(navigateMock).toHaveBeenCalledWith({
|
||||
search: "employeeTeamId=team-1"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
export const LABOR_TYPES = [
|
||||
"LAA",
|
||||
"LAB",
|
||||
"LAD",
|
||||
"LAE",
|
||||
"LAF",
|
||||
"LAG",
|
||||
"LAM",
|
||||
"LAR",
|
||||
"LAS",
|
||||
"LAU",
|
||||
"LA1",
|
||||
"LA2",
|
||||
"LA3",
|
||||
"LA4"
|
||||
];
|
||||
|
||||
export const normalizeTeamMember = (teamMember = {}) => ({
|
||||
...teamMember,
|
||||
payout_method: teamMember.payout_method || "hourly",
|
||||
labor_rates: teamMember.labor_rates || {},
|
||||
commission_rates: teamMember.commission_rates || {}
|
||||
});
|
||||
|
||||
export const normalizeEmployeeTeam = (employeeTeam = {}) => ({
|
||||
...employeeTeam,
|
||||
employee_team_members: (employeeTeam.employee_team_members || []).map(normalizeTeamMember)
|
||||
});
|
||||
|
||||
export const getSplitTotal = (teamMembers = []) =>
|
||||
teamMembers.reduce((sum, member) => sum + Number(member?.percentage || 0), 0);
|
||||
|
||||
export const hasExactSplitTotal = (teamMembers = []) => Math.abs(getSplitTotal(teamMembers) - 100) < 0.00001;
|
||||
|
||||
export const validateEmployeeTeamMembers = (employeeTeamMembers = []) => {
|
||||
const normalizedTeamMembers = employeeTeamMembers.map((teamMember) => {
|
||||
const nextTeamMember = normalizeTeamMember({ ...teamMember });
|
||||
delete nextTeamMember.__typename;
|
||||
return nextTeamMember;
|
||||
});
|
||||
|
||||
if (normalizedTeamMembers.length === 0) {
|
||||
return {
|
||||
normalizedTeamMembers,
|
||||
errorKey: "employee_teams.errors.minimum_one_member"
|
||||
};
|
||||
}
|
||||
|
||||
const employeeIds = normalizedTeamMembers.map((teamMember) => teamMember.employeeid).filter(Boolean);
|
||||
const duplicateEmployeeIds = employeeIds.filter((employeeId, index) => employeeIds.indexOf(employeeId) !== index);
|
||||
|
||||
if (duplicateEmployeeIds.length > 0) {
|
||||
return {
|
||||
normalizedTeamMembers,
|
||||
errorKey: "employee_teams.errors.duplicate_member"
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasExactSplitTotal(normalizedTeamMembers)) {
|
||||
return {
|
||||
normalizedTeamMembers,
|
||||
errorKey: "employee_teams.errors.allocation_total_exact"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
normalizedTeamMembers,
|
||||
errorKey: null
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getSplitTotal,
|
||||
hasExactSplitTotal,
|
||||
normalizeTeamMember,
|
||||
validateEmployeeTeamMembers
|
||||
} from "./shop-employee-teams.form.utils.js";
|
||||
|
||||
describe("shop employee team form utilities", () => {
|
||||
it("normalizes missing payout defaults for a team member", () => {
|
||||
expect(
|
||||
normalizeTeamMember({
|
||||
employeeid: "emp-1",
|
||||
percentage: 100
|
||||
})
|
||||
).toEqual({
|
||||
employeeid: "emp-1",
|
||||
percentage: 100,
|
||||
payout_method: "hourly",
|
||||
labor_rates: {},
|
||||
commission_rates: {}
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a minimum-member validation error when no team members are provided", () => {
|
||||
expect(validateEmployeeTeamMembers([])).toEqual({
|
||||
normalizedTeamMembers: [],
|
||||
errorKey: "employee_teams.errors.minimum_one_member"
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects duplicate employees in the same team", () => {
|
||||
const result = validateEmployeeTeamMembers([
|
||||
{ employeeid: "emp-1", percentage: 50, labor_rates: { LAA: 25 } },
|
||||
{ employeeid: "emp-1", percentage: 50, labor_rates: { LAA: 30 } }
|
||||
]);
|
||||
|
||||
expect(result.errorKey).toBe("employee_teams.errors.duplicate_member");
|
||||
});
|
||||
|
||||
it("rejects team allocations that do not add up to exactly one hundred percent", () => {
|
||||
const result = validateEmployeeTeamMembers([
|
||||
{ employeeid: "emp-1", percentage: 60, labor_rates: { LAA: 25 } },
|
||||
{ employeeid: "emp-2", percentage: 30, labor_rates: { LAA: 30 } }
|
||||
]);
|
||||
|
||||
expect(getSplitTotal(result.normalizedTeamMembers)).toBe(90);
|
||||
expect(hasExactSplitTotal(result.normalizedTeamMembers)).toBe(false);
|
||||
expect(result.errorKey).toBe("employee_teams.errors.allocation_total_exact");
|
||||
});
|
||||
|
||||
it("accepts a valid mixed hourly and commission team and strips graph metadata", () => {
|
||||
const result = validateEmployeeTeamMembers([
|
||||
{
|
||||
__typename: "employee_team_members",
|
||||
employeeid: "emp-1",
|
||||
percentage: 40,
|
||||
labor_rates: { LAA: 28.5 }
|
||||
},
|
||||
{
|
||||
employeeid: "emp-2",
|
||||
percentage: 60,
|
||||
payout_method: "commission",
|
||||
commission_rates: { LAA: 35 }
|
||||
}
|
||||
]);
|
||||
|
||||
expect(result.errorKey).toBeNull();
|
||||
expect(result.normalizedTeamMembers).toEqual([
|
||||
{
|
||||
employeeid: "emp-1",
|
||||
percentage: 40,
|
||||
payout_method: "hourly",
|
||||
labor_rates: { LAA: 28.5 },
|
||||
commission_rates: {}
|
||||
},
|
||||
{
|
||||
employeeid: "emp-2",
|
||||
percentage: 60,
|
||||
payout_method: "commission",
|
||||
labor_rates: {},
|
||||
commission_rates: { LAA: 35 }
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Form } from "antd";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { TimeTicketTaskModalComponent } from "./time-ticket-task-modal.component.jsx";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key, values = {}) => {
|
||||
const translations = {
|
||||
"timetickets.fields.ro_number": "RO Number",
|
||||
"timetickets.labels.task": "Task",
|
||||
"bodyshop.fields.md_tasks_presets.percent": "Percent",
|
||||
"bodyshop.fields.md_tasks_presets.hourstype": "Labor Types",
|
||||
"bodyshop.fields.md_tasks_presets.nextstatus": "Next Status",
|
||||
"timetickets.labels.claimtaskpreview": "Claim Task Preview",
|
||||
"timetickets.fields.employee": "Employee",
|
||||
"timetickets.fields.cost_center": "Cost Center",
|
||||
"timetickets.fields.ciecacode": "Labor Type",
|
||||
"timetickets.fields.productivehrs": "Hours",
|
||||
"timetickets.fields.payout_method": "Payout Method",
|
||||
"timetickets.fields.rate": "Rate",
|
||||
"timetickets.fields.amount": "Amount",
|
||||
"timetickets.labels.payout_methods.commission": "Commission",
|
||||
"timetickets.labels.payout_methods.hourly": "Hourly",
|
||||
"timetickets.labels.payrollclaimedtasks": "Payroll claimed tasks are ready.",
|
||||
"tt_approvals.labels.approval_queue_in_use": "Approval queue is enabled."
|
||||
};
|
||||
|
||||
if (key === "timetickets.validation.unassignedlines") {
|
||||
return `${values.unassignedHours} hours remain unassigned.`;
|
||||
}
|
||||
|
||||
return translations[key] || key;
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock("../form-items-formatted/read-only-form-item.component", () => ({
|
||||
default: ({ value }) => <span>{value}</span>
|
||||
}));
|
||||
|
||||
vi.mock("../job-search-select/job-search-select.component", () => ({
|
||||
default: () => <div>Job Search</div>
|
||||
}));
|
||||
|
||||
function TestHarness({ unassignedHours = 0 }) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={{
|
||||
task: "Body Prep",
|
||||
timetickets: [
|
||||
{
|
||||
employeeid: "emp-1",
|
||||
cost_center: "Body",
|
||||
ciecacode: "LAA",
|
||||
productivehrs: 2,
|
||||
rate: 40,
|
||||
payoutamount: 80,
|
||||
payout_context: {
|
||||
payout_method: "commission"
|
||||
}
|
||||
},
|
||||
{
|
||||
employeeid: "emp-2",
|
||||
cost_center: "Refinish",
|
||||
ciecacode: "LAB",
|
||||
productivehrs: 1,
|
||||
rate: 28,
|
||||
payoutamount: 28,
|
||||
payout_context: {
|
||||
payout_method: "hourly"
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
<TimeTicketTaskModalComponent
|
||||
bodyshop={{
|
||||
md_tasks_presets: {
|
||||
presets: [
|
||||
{
|
||||
name: "Body Prep",
|
||||
percent: 50,
|
||||
hourstype: ["LAA", "LAB"],
|
||||
nextstatus: "In Progress"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
form={form}
|
||||
loading={false}
|
||||
completedTasks={[]}
|
||||
unassignedHours={unassignedHours}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
describe("TimeTicketTaskModalComponent", () => {
|
||||
it("shows preview payout methods for both commission and hourly tickets", () => {
|
||||
render(<TestHarness />);
|
||||
|
||||
expect(screen.getByText("Claim Task Preview")).toBeInTheDocument();
|
||||
expect(screen.getByText("Commission")).toBeInTheDocument();
|
||||
expect(screen.getByText("Hourly")).toBeInTheDocument();
|
||||
expect(screen.getByText("Payroll claimed tasks are ready.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the unassigned-hours alert when payroll assignments are incomplete", () => {
|
||||
render(<TestHarness unassignedHours={1.25} />);
|
||||
|
||||
expect(screen.getByText("1.25 hours remain unassigned.")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
140
client/tests/e2e/commission-based-cut.e2e.js
Normal file
140
client/tests/e2e/commission-based-cut.e2e.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { acceptEulaIfPresent, login } from "./utils/login";
|
||||
|
||||
async function openCommissionCutHarness(page) {
|
||||
await page.goto("/manage/_test?fixture=commission-cut");
|
||||
await acceptEulaIfPresent(page);
|
||||
await expect(page.getByRole("heading", { name: "Commission Cut Test Harness" })).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe("Commission-based cut", () => {
|
||||
test.skip(!process.env.TEST_USERNAME || !process.env.TEST_PASSWORD, "Requires TEST_USERNAME and TEST_PASSWORD.");
|
||||
|
||||
test("renders payout previews and completes Pay All from the commission-cut harness", async ({ page }) => {
|
||||
let calculateLaborCalls = 0;
|
||||
let payAllCalls = 0;
|
||||
|
||||
await login(page, {
|
||||
email: process.env.TEST_USERNAME,
|
||||
password: process.env.TEST_PASSWORD
|
||||
});
|
||||
|
||||
await page.route("**/payroll/calculatelabor", async (route) => {
|
||||
calculateLaborCalls += 1;
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([
|
||||
{
|
||||
employeeid: "emp-1",
|
||||
mod_lbr_ty: "LAA",
|
||||
expectedHours: 4,
|
||||
claimedHours: 1
|
||||
},
|
||||
{
|
||||
employeeid: "emp-2",
|
||||
mod_lbr_ty: "LAB",
|
||||
expectedHours: 2,
|
||||
claimedHours: 1
|
||||
}
|
||||
])
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/payroll/payall", async (route) => {
|
||||
payAllCalls += 1;
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([{ id: "tt-1" }])
|
||||
});
|
||||
});
|
||||
|
||||
await openCommissionCutHarness(page);
|
||||
await expect(page.getByText("Claim Task Preview")).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: "Commission" })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: "Hourly" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
"There are currently 1.25 hours of repair lines that are unassigned. These hours are not including in the above calculations and must be paid manually."
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Pay All" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Pay All" }).click();
|
||||
|
||||
await expect.poll(() => calculateLaborCalls).toBeGreaterThan(0);
|
||||
await expect.poll(() => payAllCalls).toBe(1);
|
||||
await expect(page.getByText("All hours paid out successfully.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows the backend error when Pay All is rejected", async ({ page }) => {
|
||||
await login(page, {
|
||||
email: process.env.TEST_USERNAME,
|
||||
password: process.env.TEST_PASSWORD
|
||||
});
|
||||
|
||||
await page.route("**/payroll/calculatelabor", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([
|
||||
{
|
||||
employeeid: "emp-1",
|
||||
mod_lbr_ty: "LAA",
|
||||
expectedHours: 4,
|
||||
claimedHours: 1
|
||||
}
|
||||
])
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/payroll/payall", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
error: "Not all hours have been assigned."
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await openCommissionCutHarness(page);
|
||||
await page.getByRole("button", { name: "Pay All" }).click();
|
||||
|
||||
await expect(page.getByText("Error flagging hours. Not all hours have been assigned.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows a negative labor difference when previously claimed hours exceed the current expected hours", async ({
|
||||
page
|
||||
}) => {
|
||||
await login(page, {
|
||||
email: process.env.TEST_USERNAME,
|
||||
password: process.env.TEST_PASSWORD
|
||||
});
|
||||
|
||||
await page.route("**/payroll/calculatelabor", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([
|
||||
{
|
||||
employeeid: "emp-1",
|
||||
mod_lbr_ty: "LAA",
|
||||
expectedHours: 2,
|
||||
claimedHours: 5
|
||||
}
|
||||
])
|
||||
});
|
||||
});
|
||||
|
||||
await openCommissionCutHarness(page);
|
||||
|
||||
await expect(page.locator("strong").filter({ hasText: "-3" }).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,48 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
const formatToday = () => {
|
||||
const today = new Date();
|
||||
const month = String(today.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(today.getDate()).padStart(2, "0");
|
||||
const year = today.getFullYear();
|
||||
return `${month}/${day}/${year}`;
|
||||
};
|
||||
|
||||
export async function acceptEulaIfPresent(page) {
|
||||
const eulaDialog = page.getByRole("dialog", { name: "Terms and Conditions" });
|
||||
|
||||
const eulaVisible =
|
||||
(await eulaDialog.isVisible().catch(() => false)) ||
|
||||
(await eulaDialog
|
||||
.waitFor({
|
||||
state: "visible",
|
||||
timeout: 5000
|
||||
})
|
||||
.then(() => true)
|
||||
.catch(() => false));
|
||||
|
||||
if (!eulaVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const markdownCard = page.locator(".eula-markdown-card");
|
||||
await markdownCard.evaluate((element) => {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
element.dispatchEvent(new Event("scroll", { bubbles: true }));
|
||||
});
|
||||
|
||||
await page.getByRole("textbox", { name: "First Name" }).fill("Codex");
|
||||
await page.getByRole("textbox", { name: "Last Name" }).fill("Tester");
|
||||
await page.getByRole("textbox", { name: "Legal Business Name" }).fill("Codex QA");
|
||||
await page.getByRole("textbox", { name: "Date Accepted" }).fill(formatToday());
|
||||
await page.getByRole("checkbox", { name: "I accept the terms and conditions of this agreement." }).check();
|
||||
|
||||
const acceptButton = page.getByRole("button", { name: "Accept EULA" });
|
||||
await expect(acceptButton).toBeEnabled({ timeout: 10000 });
|
||||
await acceptButton.click();
|
||||
await expect(eulaDialog).not.toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
|
||||
export async function login(page, { email, password }) {
|
||||
// Navigate to the login page
|
||||
await page.goto("/"); // Adjust if your login route differs (e.g., '/login')
|
||||
@@ -16,6 +59,8 @@ export async function login(page, { email, password }) {
|
||||
// Wait for navigation or success indicator (e.g., redirect to /manage/)
|
||||
await page.waitForURL(/\/manage\//, { timeout: 10000 }); // Adjust based on redirect
|
||||
|
||||
await acceptEulaIfPresent(page);
|
||||
|
||||
// Verify successful login (e.g., check for a dashboard element)
|
||||
await expect(page.locator("text=Manage")).toBeVisible(); // Adjust to your app’s post-login UI
|
||||
}
|
||||
|
||||
@@ -1,5 +1,45 @@
|
||||
import { afterEach } from "vitest";
|
||||
import { afterEach, vi } from "vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
if (!window.matchMedia) {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn()
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.ResizeObserver) {
|
||||
window.ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
}
|
||||
|
||||
if (!window.IntersectionObserver) {
|
||||
window.IntersectionObserver = class IntersectionObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
}
|
||||
|
||||
if (!window.scrollTo) {
|
||||
window.scrollTo = vi.fn();
|
||||
}
|
||||
|
||||
if (!HTMLElement.prototype.scrollIntoView) {
|
||||
HTMLElement.prototype.scrollIntoView = vi.fn();
|
||||
}
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
Reference in New Issue
Block a user