402 lines
16 KiB
JavaScript
402 lines
16 KiB
JavaScript
import { renderHook, act } from '@testing-library/react';
|
|
|
|
jest.mock('@/lib/doormileHooks', () => ({
|
|
__esModule: true,
|
|
useHubs: jest.fn(),
|
|
useTenantLocations: jest.fn()
|
|
}));
|
|
jest.mock('@/lib/AuthContext', () => ({ __esModule: true, useAuth: jest.fn() }));
|
|
|
|
import { ZoneProvider, useZone } from '@/lib/ZoneContext';
|
|
import { useHubs, useTenantLocations } from '@/lib/doormileHooks';
|
|
import { useAuth } from '@/lib/AuthContext';
|
|
|
|
const ZONE_STORAGE_KEY = 'doormile_active_zone_id';
|
|
|
|
const KORAMANGALA = {
|
|
hubid: '2',
|
|
hubname: 'Koramangala Hub',
|
|
city: 'Bengaluru',
|
|
pincode: '560034',
|
|
latitude: 12.9352,
|
|
longitude: 77.6245,
|
|
status: 'Active'
|
|
};
|
|
|
|
const wrapper = ({ children }) => <ZoneProvider>{children}</ZoneProvider>;
|
|
|
|
const setup = ({ user = { tenantid: '0' }, hubs = [KORAMANGALA], locations = [] } = {}) => {
|
|
useAuth.mockReturnValue({ user });
|
|
useHubs.mockReturnValue({ data: hubs, isLoading: false, refetch: jest.fn() });
|
|
useTenantLocations.mockReturnValue({ data: locations, isLoading: false, refetch: jest.fn() });
|
|
return renderHook(() => useZone(), { wrapper });
|
|
};
|
|
|
|
/**
|
|
* Zone scoping — which hub or kitchen an operator is currently looking at, and
|
|
* which rows belong to it.
|
|
*
|
|
* This is a data-visibility boundary, not just a filter: on a tenant login it
|
|
* decides whether another tenant's orders can appear on screen at all. The
|
|
* tenant check therefore has its own tests, separate from the hub matching.
|
|
*/
|
|
describe('ZoneContext', () => {
|
|
describe('zone list — staff login', () => {
|
|
it('should offer All Zones plus every active hub', () => {
|
|
const { result } = setup({ hubs: [KORAMANGALA, { ...KORAMANGALA, hubid: '3', hubname: 'HSR Hub' }] });
|
|
expect(result.current.zones.map((z) => z.hubid)).toEqual(['all', '2', '3']);
|
|
});
|
|
|
|
it('should hide an inactive hub', () => {
|
|
const { result } = setup({ hubs: [KORAMANGALA, { ...KORAMANGALA, hubid: '9', status: 'Inactive' }] });
|
|
expect(result.current.zones.map((z) => z.hubid)).toEqual(['all', '2']);
|
|
});
|
|
|
|
it('should treat a hub with no status as active', () => {
|
|
const { result } = setup({ hubs: [{ ...KORAMANGALA, status: undefined }] });
|
|
expect(result.current.zones).toHaveLength(2);
|
|
});
|
|
|
|
it('should still offer All Zones when there are no hubs at all', () => {
|
|
const { result } = setup({ hubs: [] });
|
|
expect(result.current.zones).toHaveLength(1);
|
|
expect(result.current.isAllZones).toBe(true);
|
|
});
|
|
|
|
it('should not load tenant locations for a staff login', () => {
|
|
setup({ user: { tenantid: '0' } });
|
|
expect(useHubs).toHaveBeenCalledWith({ enabled: true });
|
|
expect(useTenantLocations).toHaveBeenCalledWith(null, { enabled: false });
|
|
});
|
|
});
|
|
|
|
describe('zone list — tenant login', () => {
|
|
const LOCATION = {
|
|
tenantlocationid: 55,
|
|
locationname: 'Indiranagar Kitchen',
|
|
city: 'Bengaluru',
|
|
latitude: 12.9784,
|
|
longitude: 77.6408
|
|
};
|
|
|
|
it('should offer the tenant kitchen hubs instead of global hubs', () => {
|
|
const { result } = setup({ user: { tenantid: '4' }, locations: [LOCATION] });
|
|
expect(result.current.isTenantUser).toBe(true);
|
|
expect(result.current.zones.map((z) => z.hubname)).toEqual(['All Zones', 'Indiranagar Kitchen']);
|
|
expect(result.current.zones[1].hubtype).toBe('kitchen_hub');
|
|
});
|
|
|
|
it('should scope the location query to the operator own tenant', () => {
|
|
setup({ user: { tenantid: '4' }, locations: [LOCATION] });
|
|
expect(useTenantLocations).toHaveBeenCalledWith('4', { enabled: true });
|
|
expect(useHubs).toHaveBeenCalledWith({ enabled: false });
|
|
});
|
|
|
|
it('should treat tenantid "0" as staff, not as a tenant', () => {
|
|
const { result } = setup({ user: { tenantid: '0' } });
|
|
expect(result.current.isTenantUser).toBe(false);
|
|
});
|
|
|
|
it('should fall back to the stored tenantid when the user record has none', () => {
|
|
localStorage.setItem('tenantid', '4');
|
|
const { result } = setup({ user: {}, locations: [LOCATION] });
|
|
expect(result.current.isTenantUser).toBe(true);
|
|
expect(result.current.tenantId).toBe('4');
|
|
});
|
|
|
|
it('should label an unnamed kitchen hub by its id', () => {
|
|
const { result } = setup({
|
|
user: { tenantid: '4' },
|
|
locations: [{ tenantlocationid: 55 }]
|
|
});
|
|
expect(result.current.zones[1].hubname).toBe('Kitchen Hub #55');
|
|
});
|
|
});
|
|
|
|
describe('zone selection', () => {
|
|
it('should default to All Zones', () => {
|
|
const { result } = setup();
|
|
expect(result.current.selectedZoneId).toBe('all');
|
|
expect(result.current.isAllZones).toBe(true);
|
|
});
|
|
|
|
it('should restore the previously selected zone from storage', () => {
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '2');
|
|
const { result } = setup();
|
|
expect(result.current.selectedZoneId).toBe('2');
|
|
expect(result.current.selectedZone.hubname).toBe('Koramangala Hub');
|
|
expect(result.current.isAllZones).toBe(false);
|
|
});
|
|
|
|
it('should persist a new selection', () => {
|
|
const { result } = setup();
|
|
act(() => result.current.setSelectedZoneId('2'));
|
|
expect(localStorage.getItem(ZONE_STORAGE_KEY)).toBe('2');
|
|
expect(result.current.selectedZone.hubid).toBe('2');
|
|
});
|
|
|
|
it('should coerce a numeric zone id to a string', () => {
|
|
const { result } = setup();
|
|
act(() => result.current.setSelectedZoneId(2));
|
|
expect(result.current.selectedZoneId).toBe('2');
|
|
expect(result.current.selectedZone.hubname).toBe('Koramangala Hub');
|
|
});
|
|
|
|
it.each([null, undefined, ''])('should fall back to All Zones for the selection %p', (id) => {
|
|
const { result } = setup();
|
|
act(() => result.current.setSelectedZoneId(id));
|
|
expect(result.current.selectedZoneId).toBe('all');
|
|
});
|
|
|
|
it('should fall back to All Zones when the stored zone no longer exists', () => {
|
|
// A hub that was deleted or deactivated must not leave the console
|
|
// filtering against a zone that matches nothing.
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '999');
|
|
const { result } = setup();
|
|
expect(result.current.selectedZone.hubid).toBe('all');
|
|
expect(result.current.isAllZones).toBe(true);
|
|
});
|
|
|
|
it('should survive a storage read failure at boot', () => {
|
|
const getItem = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
throw new Error('storage disabled');
|
|
});
|
|
expect(() => setup()).not.toThrow();
|
|
getItem.mockRestore();
|
|
});
|
|
|
|
it('should not throw when persisting is refused', () => {
|
|
const setItem = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
throw new Error('QuotaExceededError');
|
|
});
|
|
const { result } = setup();
|
|
expect(() => act(() => result.current.setSelectedZoneId('2'))).not.toThrow();
|
|
expect(result.current.selectedZoneId).toBe('2');
|
|
setItem.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('matchesZone — All Zones', () => {
|
|
it('should match any row', () => {
|
|
const { result } = setup();
|
|
expect(result.current.matchesZone({ hubid: '999' })).toBe(true);
|
|
expect(result.current.matchesZone({})).toBe(true);
|
|
});
|
|
|
|
it('should still reject a null row', () => {
|
|
const { result } = setup();
|
|
expect(result.current.matchesZone(null)).toBe(false);
|
|
expect(result.current.matchesZone(undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('matchesZone — tenant isolation', () => {
|
|
it('should reject a row belonging to a different tenant even under All Zones', () => {
|
|
// The data-visibility boundary: tenant scoping is checked BEFORE the
|
|
// all-zones shortcut, so another tenant's orders can never render.
|
|
localStorage.setItem('tenantid', '4');
|
|
const { result } = setup({ user: { tenantid: '4' }, locations: [{ tenantlocationid: 55 }] });
|
|
|
|
expect(result.current.isAllZones).toBe(true);
|
|
expect(result.current.matchesZone({ tenantid: '9' })).toBe(false);
|
|
expect(result.current.matchesZone({ tenantid: '4' })).toBe(true);
|
|
});
|
|
|
|
it('should compare tenant ids across number and string forms', () => {
|
|
const { result } = setup({ user: { tenantid: '4' }, locations: [{ tenantlocationid: 55 }] });
|
|
expect(result.current.matchesZone({ tenantid: 4 })).toBe(true);
|
|
});
|
|
|
|
it('should not apply tenant scoping to a staff login', () => {
|
|
const { result } = setup({ user: { tenantid: '0' } });
|
|
expect(result.current.matchesZone({ tenantid: '9' })).toBe(true);
|
|
});
|
|
|
|
it('should allow a row that carries no tenant id at all', () => {
|
|
const { result } = setup({ user: { tenantid: '4' }, locations: [{ tenantlocationid: 55 }] });
|
|
expect(result.current.matchesZone({ bookingid: 1 })).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('matchesZone — hub matching for orders', () => {
|
|
const selectKoramangala = () => {
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '2');
|
|
return setup();
|
|
};
|
|
|
|
it.each(['hubid', 'sourcehubid', 'applocationid', 'tenantlocationid', 'locationid'])(
|
|
'should match on a direct %s',
|
|
(field) => {
|
|
const { result } = selectKoramangala();
|
|
expect(result.current.matchesZone({ [field]: '2' })).toBe(true);
|
|
}
|
|
);
|
|
|
|
it('should match a numeric hub id against the string zone id', () => {
|
|
const { result } = selectKoramangala();
|
|
expect(result.current.matchesZone({ hubid: 2 })).toBe(true);
|
|
});
|
|
|
|
it('should match on the hub pincode appearing in an address', () => {
|
|
const { result } = selectKoramangala();
|
|
expect(result.current.matchesZone({ deliveryaddress: '4th Block, Bengaluru 560034' })).toBe(true);
|
|
});
|
|
|
|
it('should match on the hub city appearing in an address, case-insensitively', () => {
|
|
const { result } = selectKoramangala();
|
|
expect(result.current.matchesZone({ pickupaddress: '12 MG Road, BENGALURU' })).toBe(true);
|
|
});
|
|
|
|
it('should match on the hub name appearing in an address', () => {
|
|
const { result } = selectKoramangala();
|
|
expect(result.current.matchesZone({ address: 'Near koramangala hub' })).toBe(true);
|
|
});
|
|
|
|
it('should match a pickup coordinate inside the 35 km radius', () => {
|
|
const { result } = selectKoramangala();
|
|
expect(result.current.matchesZone({ pickuplatitude: 12.94, pickuplongitude: 77.62 })).toBe(true);
|
|
});
|
|
|
|
it('should match a drop coordinate inside the radius even when pickup is far', () => {
|
|
const { result } = selectKoramangala();
|
|
expect(
|
|
result.current.matchesZone({
|
|
pickuplatitude: 19.076,
|
|
pickuplongitude: 72.877,
|
|
deliverylatitude: 12.94,
|
|
deliverylongitude: 77.62
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should reject a row in another city with no matching id or coordinate', () => {
|
|
const { result } = selectKoramangala();
|
|
expect(
|
|
result.current.matchesZone({
|
|
hubid: '7',
|
|
deliveryaddress: 'Bandra West, Mumbai 400050',
|
|
deliverylatitude: 19.076,
|
|
deliverylongitude: 72.877
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it('should reject a coordinate well outside the radius', () => {
|
|
const { result } = selectKoramangala();
|
|
expect(result.current.matchesZone({ latitude: 19.076, longitude: 72.877 })).toBe(false);
|
|
});
|
|
|
|
it('should reject a row with nothing to match on', () => {
|
|
const { result } = selectKoramangala();
|
|
expect(result.current.matchesZone({ bookingid: 4021 })).toBe(false);
|
|
});
|
|
|
|
it('should ignore an unusable coordinate rather than treating it as origin', () => {
|
|
// (0,0) is in the Atlantic; a NaN or null coordinate must not silently
|
|
// become a position that is 35 km from nothing.
|
|
const { result } = selectKoramangala();
|
|
expect(result.current.matchesZone({ pickuplatitude: null, pickuplongitude: null })).toBe(false);
|
|
expect(result.current.matchesZone({ pickuplatitude: 'abc', pickuplongitude: 'abc' })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('matchesZone — riders', () => {
|
|
it('should match a rider assigned to the selected hub', () => {
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '2');
|
|
const { result } = setup();
|
|
expect(result.current.matchesZone({ milerprofileid: 11, hubid: '2' })).toBe(true);
|
|
});
|
|
|
|
it('should match a rider by their app location', () => {
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '2');
|
|
const { result } = setup();
|
|
expect(result.current.matchesZone({ milerprofileid: 11, applocationid: '2' })).toBe(true);
|
|
});
|
|
|
|
it('should match a rider by city when they carry no hub id', () => {
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '2');
|
|
const { result } = setup();
|
|
expect(result.current.matchesZone({ milerprofileid: 11, city: 'bengaluru' })).toBe(true);
|
|
});
|
|
|
|
it('should reject a rider from another city', () => {
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '2');
|
|
const { result } = setup();
|
|
expect(result.current.matchesZone({ milerprofileid: 11, city: 'Mumbai' })).toBe(false);
|
|
});
|
|
|
|
it('should show every fleet rider to a tenant, whichever kitchen is selected', () => {
|
|
// Riders belong to the tenant fleet and serve all its kitchen hubs.
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '55');
|
|
const { result } = setup({
|
|
user: { tenantid: '4' },
|
|
locations: [{ tenantlocationid: 55, city: 'Bengaluru' }]
|
|
});
|
|
expect(result.current.matchesZone({ milerprofileid: 11, tenantid: '4', city: 'Mysuru' })).toBe(true);
|
|
});
|
|
|
|
it('should still reject a rider belonging to another tenant', () => {
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '55');
|
|
const { result } = setup({
|
|
user: { tenantid: '4' },
|
|
locations: [{ tenantlocationid: 55, city: 'Bengaluru' }]
|
|
});
|
|
expect(result.current.matchesZone({ milerprofileid: 11, tenantid: '9' })).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
['milerprofileid', { milerprofileid: 11 }],
|
|
['authname', { authname: 'anita' }],
|
|
['userid + availabilitystatus', { userid: 91, availabilitystatus: 'available' }]
|
|
])('should recognise a rider row by its %s', (_label, row) => {
|
|
localStorage.setItem(ZONE_STORAGE_KEY, '2');
|
|
const { result } = setup();
|
|
// A rider row with no location signal is rejected as a rider, not
|
|
// accidentally address-matched as an order.
|
|
expect(result.current.matchesZone(row)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('loading state and refetch', () => {
|
|
it('should surface the hub loading state for a staff login', () => {
|
|
useAuth.mockReturnValue({ user: { tenantid: '0' } });
|
|
useHubs.mockReturnValue({ data: [], isLoading: true, refetch: jest.fn() });
|
|
useTenantLocations.mockReturnValue({ data: [], isLoading: false, refetch: jest.fn() });
|
|
|
|
const { result } = renderHook(() => useZone(), { wrapper });
|
|
expect(result.current.isHubsLoading).toBe(true);
|
|
});
|
|
|
|
it('should surface the location loading state for a tenant login', () => {
|
|
useAuth.mockReturnValue({ user: { tenantid: '4' } });
|
|
useHubs.mockReturnValue({ data: [], isLoading: false, refetch: jest.fn() });
|
|
useTenantLocations.mockReturnValue({ data: [], isLoading: true, refetch: jest.fn() });
|
|
|
|
const { result } = renderHook(() => useZone(), { wrapper });
|
|
expect(result.current.isHubsLoading).toBe(true);
|
|
});
|
|
|
|
it('should refetch the source the current login actually reads', () => {
|
|
const refetchHubs = jest.fn();
|
|
const refetchLocations = jest.fn();
|
|
useAuth.mockReturnValue({ user: { tenantid: '4' } });
|
|
useHubs.mockReturnValue({ data: [], isLoading: false, refetch: refetchHubs });
|
|
useTenantLocations.mockReturnValue({ data: [], isLoading: false, refetch: refetchLocations });
|
|
|
|
const { result } = renderHook(() => useZone(), { wrapper });
|
|
act(() => result.current.refetchHubs());
|
|
|
|
expect(refetchLocations).toHaveBeenCalledTimes(1);
|
|
expect(refetchHubs).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('useZone outside a provider', () => {
|
|
it('should throw a named error rather than returning undefined', () => {
|
|
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
expect(() => renderHook(() => useZone())).toThrow('useZone must be used within a ZoneProvider');
|
|
spy.mockRestore();
|
|
});
|
|
});
|
|
});
|