126 lines
5.0 KiB
JavaScript
126 lines
5.0 KiB
JavaScript
import React from 'react';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import SlaRemediationCard from '@/components/doormile/SlaRemediationCard';
|
|
|
|
// lucide-react ships ESM Jest does not transform; every component test here stubs it.
|
|
jest.mock('lucide-react', () =>
|
|
new Proxy({}, { get: (_t, prop) => (prop === '__esModule' ? true : (props) => <span data-testid={`icon-${String(prop)}`} {...props} />) })
|
|
);
|
|
|
|
jest.mock('@/api/doormile/notify', () => ({
|
|
OpenToast: jest.fn()
|
|
}));
|
|
|
|
describe('SlaRemediationCard Component', () => {
|
|
const sampleFinding = {
|
|
title: '3 orders overdue in RS Puram',
|
|
why: 'Parcels have exceeded their promised delivery SLA window by >45 minutes.',
|
|
severity: 'critical',
|
|
count: 3,
|
|
proposal: {
|
|
label: 'Split between Karthik & Suresh',
|
|
blastRadius: 'Will transfer 2 orders to Karthik and 1 order to Suresh.'
|
|
}
|
|
};
|
|
|
|
it('renders title, explanation, and proposal details properly', () => {
|
|
render(<SlaRemediationCard finding={sampleFinding} />);
|
|
expect(screen.getByText('3 orders overdue in RS Puram')).toBeInTheDocument();
|
|
expect(screen.getByText(/Parcels have exceeded their promised delivery SLA window/)).toBeInTheDocument();
|
|
expect(screen.getByText('Needs attention now')).toBeInTheDocument();
|
|
expect(screen.getByText('3 Orders')).toBeInTheDocument();
|
|
expect(screen.getAllByText('Split between Karthik & Suresh').length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('triggers onRemediate callback when Apply button is clicked', async () => {
|
|
const handleRemediate = jest.fn(async () => {});
|
|
render(<SlaRemediationCard finding={sampleFinding} onRemediate={handleRemediate} />);
|
|
|
|
const button = screen.getByRole('button', { name: /Split between Karthik & Suresh/i });
|
|
fireEvent.click(button);
|
|
|
|
await waitFor(() => {
|
|
expect(handleRemediate).toHaveBeenCalledTimes(1);
|
|
expect(handleRemediate).toHaveBeenCalledWith(sampleFinding);
|
|
expect(screen.getByText('Action sent')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('returns null if no finding is passed', () => {
|
|
const { container } = render(<SlaRemediationCard finding={null} />);
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('SlaRemediationCard — never claims an action it did not perform', () => {
|
|
const { OpenToast } = require('@/api/doormile/notify');
|
|
|
|
const finding = {
|
|
title: '1 rider stalled at door',
|
|
why: 'Rider stamped arrival but delivery has not completed.',
|
|
severity: 'critical',
|
|
count: 1,
|
|
proposal: {
|
|
label: 'Message waiting riders',
|
|
blastRadius: 'Sends an assistance check-in to stalled riders.'
|
|
}
|
|
};
|
|
|
|
beforeEach(() => {
|
|
OpenToast.mockClear();
|
|
});
|
|
|
|
// The bug: with no onRemediate the handler fell through to a 600ms timer and
|
|
// then reported "Remediation executed" with a green card. No caller has ever
|
|
// passed onRemediate, so every click on the Exceptions page took that branch
|
|
// and told the operator a rider had been messaged when nothing was sent.
|
|
it('does not report success when no executor is wired', async () => {
|
|
render(<SlaRemediationCard finding={finding} />);
|
|
|
|
const button = screen.getByRole('button', { name: /review only/i });
|
|
expect(button).toBeDisabled();
|
|
|
|
fireEvent.click(button);
|
|
|
|
await waitFor(() => {
|
|
expect(OpenToast).not.toHaveBeenCalled();
|
|
});
|
|
expect(screen.queryByText(/remediation executed/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('labels the action as review-only rather than offering a fix', () => {
|
|
render(<SlaRemediationCard finding={finding} />);
|
|
expect(screen.getByRole('button', { name: /review only/i })).toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: /message waiting riders/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('executes and reports success only when an executor IS supplied', async () => {
|
|
const onRemediate = jest.fn().mockResolvedValue({ ok: true, message: 'Notified 1 rider.' });
|
|
render(<SlaRemediationCard finding={finding} onRemediate={onRemediate} />);
|
|
|
|
const button = screen.getByRole('button', { name: /message waiting riders/i });
|
|
expect(button).toBeEnabled();
|
|
|
|
fireEvent.click(button);
|
|
|
|
await waitFor(() => {
|
|
expect(onRemediate).toHaveBeenCalledWith(finding);
|
|
// (message, variant) — the branch passed ('success', msg), which rendered a RED error toast reading "success".
|
|
// The executor's own message is shown, so a partial success stays visible.
|
|
expect(OpenToast).toHaveBeenCalledWith('Notified 1 rider.', 'success');
|
|
});
|
|
});
|
|
|
|
it('reports failure honestly when the executor throws', async () => {
|
|
const onRemediate = jest.fn().mockRejectedValue(new Error('network'));
|
|
render(<SlaRemediationCard finding={finding} onRemediate={onRemediate} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /message waiting riders/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(OpenToast).toHaveBeenCalledWith('network', 'error');
|
|
});
|
|
expect(OpenToast).not.toHaveBeenCalledWith(expect.anything(), 'success');
|
|
});
|
|
});
|