Files
doormilxpress_astryx/tests/lib/ZoneContext.test.jsx
2026-08-27 13:40:48 +05:30

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();
});
});
});