481 lines
20 KiB
JavaScript
481 lines
20 KiB
JavaScript
import {
|
|
ANCHOR_RADIUS_M,
|
|
DROP_TARGET,
|
|
FLOW,
|
|
PICKUP_SOURCE,
|
|
PICKUP_SOURCE_TYPE,
|
|
buildAnchors,
|
|
buildFlowFields,
|
|
classifyBooking,
|
|
flowForDraft,
|
|
flowLabel,
|
|
hasCustomerPickup,
|
|
hasHubDrop,
|
|
matchAnchor,
|
|
normalizeFlow
|
|
} from '@/lib/orderFlow';
|
|
|
|
/**
|
|
* Which end of a booking is a Doormile facility.
|
|
*
|
|
* The bug these tests exist to prevent: the console assumed the pickup was
|
|
* ALWAYS the hub. queries.js used the tenant's name as the collection point's
|
|
* identity, so every customer pickup for one client collapsed onto a single map
|
|
* pin placed at an arbitrary one of them, and the rider's polyline skipped the
|
|
* entire first-mile leg.
|
|
*
|
|
* Two invariants carry the most weight here and are asserted repeatedly:
|
|
*
|
|
* 1. UNKNOWABLE MEANS FORWARD. No anchors loaded, no coordinates, a 0,0
|
|
* "unset" — none of these may produce a customer pickup. An order that
|
|
* cannot be classified has to look exactly as it did before this module
|
|
* existed, or a data problem starts drawing phantom collection points.
|
|
* 2. A CUSTOMER PICKUP NEVER SHARES A GROUPING KEY. Two doorsteps are two
|
|
* places. Merging them is precisely the defect being replaced.
|
|
*/
|
|
|
|
// Real Coimbatore-ish coordinates, so the distances under test are the
|
|
// magnitudes the radius was actually chosen for.
|
|
const HUB = { hubid: 7, hubname: 'Gandhipuram Hub', latitude: 11.0168, longitude: 76.9558 };
|
|
const KITCHEN = {
|
|
tenantlocationid: 31,
|
|
tenantid: 4,
|
|
locationname: '124 Gandhipuram Main Rd',
|
|
latitude: 11.021,
|
|
longitude: 76.962
|
|
};
|
|
|
|
// ~4 km and ~6 km out — unambiguously somebody's door.
|
|
const DOOR_A = { lat: 11.05, lng: 77.0 };
|
|
const DOOR_B = { lat: 11.06, lng: 77.01 };
|
|
|
|
const anchors = buildAnchors({ hubs: [HUB], tenantLocations: [KITCHEN] });
|
|
|
|
const booking = ({ id = 1, from, to, ...rest }) => ({
|
|
bookingid: id,
|
|
pickuplatitude: from.lat,
|
|
pickuplongitude: from.lng,
|
|
deliverylatitude: to.lat,
|
|
deliverylongitude: to.lng,
|
|
...rest
|
|
});
|
|
|
|
const AT_HUB = { lat: HUB.latitude, lng: HUB.longitude };
|
|
const AT_KITCHEN = { lat: KITCHEN.latitude, lng: KITCHEN.longitude };
|
|
|
|
describe('orderFlow', () => {
|
|
describe('buildAnchors', () => {
|
|
it('should treat both a Doormile hub and a client location as facilities', () => {
|
|
expect(anchors).toHaveLength(2);
|
|
expect(anchors.map((a) => a.kind).sort()).toEqual(['hub', 'tenantlocation']);
|
|
});
|
|
|
|
it('should give each anchor a key that is unique per facility', () => {
|
|
expect(new Set(anchors.map((a) => a.key)).size).toBe(anchors.length);
|
|
});
|
|
|
|
it('should drop a facility nobody geocoded rather than keep it with NaN coordinates', () => {
|
|
const built = buildAnchors({ hubs: [{ hubid: 9, hubname: 'Unmapped' }, HUB] });
|
|
expect(built.map((a) => a.hubid)).toEqual([7]);
|
|
});
|
|
|
|
it('should return an empty set when called with nothing', () => {
|
|
expect(buildAnchors()).toEqual([]);
|
|
});
|
|
|
|
it('should give a tenant location the hub that services its city', () => {
|
|
// A tenant location record has NO hubid field — the API does not carry
|
|
// one. Without this, every order originating at a client's own kitchen
|
|
// went to the backend with no servicing hub, and zone attribution fell
|
|
// straight back to matching hub names against address text.
|
|
const kitchen = anchors.find((a) => a.kind === 'tenantlocation');
|
|
expect(kitchen.hubid).toBe(7);
|
|
});
|
|
|
|
it('should not invent a servicing hub for a location in another city', () => {
|
|
// Chennai, ~400 km away. A wrong hub id is worse than none: it files the
|
|
// order into a zone it has nothing to do with, where nobody is looking.
|
|
const [faraway] = buildAnchors({
|
|
hubs: [HUB],
|
|
tenantLocations: [{ tenantlocationid: 99, locationname: 'Chennai Depot', latitude: 13.0827, longitude: 80.2707 }]
|
|
}).filter((a) => a.kind === 'tenantlocation');
|
|
expect(faraway.hubid).toBeNull();
|
|
});
|
|
|
|
it("should keep a location's own hubid when the record already has one", () => {
|
|
const [explicit] = buildAnchors({
|
|
hubs: [HUB],
|
|
tenantLocations: [{ ...KITCHEN, hubid: 12 }]
|
|
}).filter((a) => a.kind === 'tenantlocation');
|
|
expect(explicit.hubid).toBe(12);
|
|
});
|
|
|
|
it('should leave hubid null when no hubs were loaded at all', () => {
|
|
const [alone] = buildAnchors({ tenantLocations: [KITCHEN] });
|
|
expect(alone.hubid).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('matchAnchor', () => {
|
|
it('should match a point sitting on a facility', () => {
|
|
expect(matchAnchor(HUB.latitude, HUB.longitude, anchors)?.hubid).toBe(7);
|
|
});
|
|
|
|
it('should not match a customer door several km away', () => {
|
|
expect(matchAnchor(DOOR_A.lat, DOOR_A.lng, anchors)).toBeNull();
|
|
});
|
|
|
|
it('should choose the NEAREST facility, not the first one in the array', () => {
|
|
// Two facilities sharing a compound is normal. Picking by array order
|
|
// would name the same pin differently depending on what /admin/hubs
|
|
// happened to return first.
|
|
const twinA = { hubid: 1, hubname: 'Sorting Centre', latitude: 11.0168, longitude: 76.9558 };
|
|
const twinB = { hubid: 2, hubname: 'Client Depot', latitude: 11.01685, longitude: 76.95585 };
|
|
const both = buildAnchors({ hubs: [twinA, twinB] });
|
|
expect(matchAnchor(twinB.latitude, twinB.longitude, both)?.hubid).toBe(2);
|
|
expect(matchAnchor(twinA.latitude, twinA.longitude, both)?.hubid).toBe(1);
|
|
});
|
|
|
|
it('should never match a 0,0 "unset" coordinate against a real facility', () => {
|
|
expect(matchAnchor(0, 0, anchors)).toBeNull();
|
|
});
|
|
|
|
it('should treat the radius as a boundary in metres, not degrees', () => {
|
|
// ~100 m north of the hub: inside. ~500 m: outside.
|
|
const inside = HUB.latitude + 0.0009;
|
|
const outside = HUB.latitude + 0.0045;
|
|
expect(matchAnchor(inside, HUB.longitude, anchors)).not.toBeNull();
|
|
expect(matchAnchor(outside, HUB.longitude, anchors)).toBeNull();
|
|
expect(ANCHOR_RADIUS_M).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('classifyBooking — the four flows', () => {
|
|
it('should classify hub to customer as Forward', () => {
|
|
const r = classifyBooking(booking({ from: AT_KITCHEN, to: DOOR_A }), anchors);
|
|
expect(r.flow).toBe(FLOW.FORWARD);
|
|
expect(r.pickupSource).toBe(PICKUP_SOURCE.HUB);
|
|
expect(r.dropTarget).toBe(DROP_TARGET.CUSTOMER);
|
|
});
|
|
|
|
it('should classify customer to hub as Reverse — the customer pickup', () => {
|
|
const r = classifyBooking(booking({ from: DOOR_A, to: AT_HUB }), anchors);
|
|
expect(r.flow).toBe(FLOW.REVERSE);
|
|
expect(r.pickupSource).toBe(PICKUP_SOURCE.CUSTOMER);
|
|
expect(r.dropTarget).toBe(DROP_TARGET.HUB);
|
|
});
|
|
|
|
it('should classify customer to customer as P2P', () => {
|
|
const r = classifyBooking(booking({ from: DOOR_A, to: DOOR_B }), anchors);
|
|
expect(r.flow).toBe(FLOW.P2P);
|
|
});
|
|
|
|
it('should classify hub to hub as Linehaul', () => {
|
|
const r = classifyBooking(booking({ from: AT_KITCHEN, to: AT_HUB }), anchors);
|
|
expect(r.flow).toBe(FLOW.LINEHAUL);
|
|
});
|
|
});
|
|
|
|
describe('classifyBooking — unknowable input stays Forward', () => {
|
|
it('should fall back to Forward when no facility list has loaded', () => {
|
|
// Geometry proves nothing without anchors. Anything but Forward here
|
|
// would relabel the whole board during the first render, before
|
|
// /admin/hubs has answered.
|
|
expect(classifyBooking(booking({ from: DOOR_A, to: DOOR_B }), []).flow).toBe(FLOW.FORWARD);
|
|
expect(classifyBooking(booking({ from: DOOR_A, to: DOOR_B })).flow).toBe(FLOW.FORWARD);
|
|
});
|
|
|
|
it('should fall back to Forward for a pickup that failed to geocode', () => {
|
|
// 0,0 is the backend's "unset". A pickup that never geocoded is a data
|
|
// problem, not a doorstep collection — calling it one would put a
|
|
// phantom violet pin on the map off the coast of Africa.
|
|
const unset = classifyBooking(booking({ id: 9, from: { lat: 0, lng: 0 }, to: DOOR_A }), anchors);
|
|
expect(unset.flow).toBe(FLOW.FORWARD);
|
|
|
|
const missing = classifyBooking({ bookingid: 10, deliverylatitude: DOOR_A.lat, deliverylongitude: DOOR_A.lng }, anchors);
|
|
expect(missing.flow).toBe(FLOW.FORWARD);
|
|
});
|
|
|
|
it('should not crash on a null or empty booking', () => {
|
|
expect(classifyBooking(null, anchors).flow).toBe(FLOW.FORWARD);
|
|
expect(classifyBooking({}, anchors).flow).toBe(FLOW.FORWARD);
|
|
});
|
|
});
|
|
|
|
describe('classifyBooking — an explicit ordertype wins over geometry', () => {
|
|
it('should trust a stored ordertype even when the coordinates disagree', () => {
|
|
// Coordinates say Forward; the record says otherwise. Once the backend
|
|
// persists the field it is the authority, because it records what the
|
|
// operator actually booked.
|
|
const r = classifyBooking(booking({ from: AT_KITCHEN, to: DOOR_A, ordertype: 'Reverse' }), anchors);
|
|
expect(r.flow).toBe(FLOW.REVERSE);
|
|
expect(r.derived).toBe(false);
|
|
});
|
|
|
|
it('should mark a geometry-decided classification as derived', () => {
|
|
expect(classifyBooking(booking({ from: DOOR_A, to: AT_HUB }), anchors).derived).toBe(true);
|
|
});
|
|
|
|
it('should fall through to geometry when the stored value means nothing', () => {
|
|
const r = classifyBooking(booking({ from: DOOR_A, to: AT_HUB, ordertype: 'banana' }), anchors);
|
|
expect(r.flow).toBe(FLOW.REVERSE);
|
|
expect(r.derived).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('classifyBooking — the pickup point grouping key', () => {
|
|
it('should give every order off one facility the SAME key', () => {
|
|
const a = classifyBooking(booking({ id: 1, from: AT_KITCHEN, to: DOOR_A }), anchors);
|
|
const b = classifyBooking(booking({ id: 2, from: AT_KITCHEN, to: DOOR_B }), anchors);
|
|
expect(a.pickupPointKey).toBe(b.pickupPointKey);
|
|
});
|
|
|
|
it('should give every customer pickup its OWN key', () => {
|
|
// The defect being replaced: these two collapsed onto one map pin
|
|
// because both resolved to the tenant's name.
|
|
const a = classifyBooking(booking({ id: 1, from: DOOR_A, to: AT_HUB }), anchors);
|
|
const b = classifyBooking(booking({ id: 2, from: DOOR_B, to: AT_HUB }), anchors);
|
|
expect(a.pickupPointKey).not.toBe(b.pickupPointKey);
|
|
});
|
|
|
|
it('should name a facility pickup after the facility and a customer pickup after its address', () => {
|
|
const hub = classifyBooking(booking({ from: AT_KITCHEN, to: DOOR_A }), anchors);
|
|
expect(hub.pickupPointName).toBe('124 Gandhipuram Main Rd');
|
|
|
|
const door = classifyBooking(
|
|
booking({ from: DOOR_A, to: AT_HUB, pickupaddress: '22 Ponvizha Nagar' }),
|
|
anchors
|
|
);
|
|
expect(door.pickupPointName).toBe('22 Ponvizha Nagar');
|
|
});
|
|
});
|
|
|
|
describe('classifyBooking — servicing hub', () => {
|
|
it('should prefer an id the booking already carries', () => {
|
|
const r = classifyBooking(booking({ from: DOOR_A, to: DOOR_B, sourcehubid: 42 }), anchors);
|
|
expect(r.servicingHubId).toBe(42);
|
|
});
|
|
|
|
it('should fall back to the DESTINATION hub on a customer pickup', () => {
|
|
// Nothing at the pickup end names a facility, but an inbound collection
|
|
// plainly belongs to the hub it is being brought to. Without this, every
|
|
// customer pickup would lose its zone.
|
|
const r = classifyBooking(booking({ from: DOOR_A, to: AT_HUB }), anchors);
|
|
expect(r.servicingHubId).toBe(7);
|
|
});
|
|
|
|
it('should resolve through a tenant location, not only through a Doormile hub', () => {
|
|
// The case that was silently broken: the pickup matches the client's own
|
|
// kitchen, which is not itself a hub. Before the kitchen inherited a
|
|
// servicing hub this returned null and the order reached the backend
|
|
// with no zone.
|
|
const r = classifyBooking(booking({ from: AT_KITCHEN, to: DOOR_A }), anchors);
|
|
expect(r.originAnchor.kind).toBe('tenantlocation');
|
|
expect(r.servicingHubId).toBe(7);
|
|
});
|
|
|
|
it('should be null on a P2P run where neither end is a facility', () => {
|
|
// Deliberate: a fabricated hub id here would file the order under a zone
|
|
// it has nothing to do with. Null lets ZoneContext fall back to radius.
|
|
expect(classifyBooking(booking({ from: DOOR_A, to: DOOR_B }), anchors).servicingHubId).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('normalizeFlow', () => {
|
|
it.each([
|
|
['reverse', FLOW.REVERSE],
|
|
['REVERSE', FLOW.REVERSE],
|
|
['customer_pickup', FLOW.REVERSE],
|
|
['customer pickup', FLOW.REVERSE],
|
|
['customer-pickup', FLOW.REVERSE],
|
|
['hub_pickup', FLOW.FORWARD],
|
|
['outbound', FLOW.FORWARD],
|
|
['p2p', FLOW.P2P],
|
|
['point_to_point', FLOW.P2P],
|
|
['hyperlocal', FLOW.P2P],
|
|
['line_haul', FLOW.LINEHAUL]
|
|
])('should read %s as %s', (input, expected) => {
|
|
expect(normalizeFlow(input)).toBe(expected);
|
|
});
|
|
|
|
it('should return null for anything it does not recognise, never throw', () => {
|
|
[undefined, null, '', ' ', 'banana', 42, {}].forEach((value) => {
|
|
expect(normalizeFlow(value)).toBeNull();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('flow predicates', () => {
|
|
it('should identify which flows require travelling out to collect', () => {
|
|
expect(hasCustomerPickup(FLOW.REVERSE)).toBe(true);
|
|
expect(hasCustomerPickup(FLOW.P2P)).toBe(true);
|
|
expect(hasCustomerPickup(FLOW.FORWARD)).toBe(false);
|
|
expect(hasCustomerPickup(FLOW.LINEHAUL)).toBe(false);
|
|
});
|
|
|
|
it('should identify which flows end at a facility', () => {
|
|
expect(hasHubDrop(FLOW.REVERSE)).toBe(true);
|
|
expect(hasHubDrop(FLOW.LINEHAUL)).toBe(true);
|
|
expect(hasHubDrop(FLOW.FORWARD)).toBe(false);
|
|
expect(hasHubDrop(FLOW.P2P)).toBe(false);
|
|
});
|
|
|
|
it('should label every flow, and fall back rather than render blank', () => {
|
|
Object.values(FLOW).forEach((f) => expect(flowLabel(f)).toBeTruthy());
|
|
expect(flowLabel(FLOW.REVERSE)).toBe('Customer Pickup');
|
|
expect(flowLabel('nonsense')).toBe(flowLabel(FLOW.FORWARD));
|
|
});
|
|
});
|
|
|
|
describe('flowForDraft — what the create form books', () => {
|
|
it('should book a hub pickup to a typed address as Forward', () => {
|
|
const r = flowForDraft({
|
|
pickupSource: PICKUP_SOURCE.HUB,
|
|
dropLat: DOOR_A.lat,
|
|
dropLng: DOOR_A.lng,
|
|
anchors
|
|
});
|
|
expect(r.flow).toBe(FLOW.FORWARD);
|
|
});
|
|
|
|
it('should recognise a drop that lands on a hub as a return, not a delivery', () => {
|
|
// Without measuring the drop end, a rider bringing a parcel back to the
|
|
// hub would be booked P2P and routed as if the hub were a house.
|
|
const r = flowForDraft({
|
|
pickupSource: PICKUP_SOURCE.CUSTOMER,
|
|
dropLat: HUB.latitude,
|
|
dropLng: HUB.longitude,
|
|
anchors
|
|
});
|
|
expect(r.flow).toBe(FLOW.REVERSE);
|
|
expect(r.destAnchor?.hubid).toBe(7);
|
|
});
|
|
|
|
it('should book a customer pickup to a customer address as P2P', () => {
|
|
const r = flowForDraft({
|
|
pickupSource: PICKUP_SOURCE.CUSTOMER,
|
|
dropLat: DOOR_B.lat,
|
|
dropLng: DOOR_B.lng,
|
|
anchors
|
|
});
|
|
expect(r.flow).toBe(FLOW.P2P);
|
|
});
|
|
|
|
it('should default to a hub pickup when the form has told it nothing', () => {
|
|
expect(flowForDraft().flow).toBe(FLOW.FORWARD);
|
|
});
|
|
});
|
|
|
|
describe('buildFlowFields — what goes on the wire', () => {
|
|
it('should always name the flow', () => {
|
|
expect(buildFlowFields({ flow: FLOW.REVERSE }).ordertype).toBe(FLOW.REVERSE);
|
|
});
|
|
|
|
it('should write BOTH hub id fields, which the console has only ever read', () => {
|
|
// queries.js has always read b.hubid / b.sourcehubid and the console has
|
|
// never written either, which is why zone attribution had to fall back to
|
|
// matching hub names against concatenated address text.
|
|
const fields = buildFlowFields({ flow: FLOW.FORWARD, servicingHubId: 7, tenantLocationId: 31 });
|
|
expect(fields.sourcehubid).toBe(7);
|
|
expect(fields.hubid).toBe(7);
|
|
expect(fields.tenantlocationid).toBe(31);
|
|
});
|
|
|
|
it('should omit ids it was not given rather than send null', () => {
|
|
// pickup_source_type is NOT an id and is always present — see the next
|
|
// block for why an omission would say the wrong thing.
|
|
expect(buildFlowFields({ flow: FLOW.P2P })).toEqual({
|
|
ordertype: FLOW.P2P,
|
|
pickup_source_type: PICKUP_SOURCE_TYPE.CUSTOMER
|
|
});
|
|
expect(buildFlowFields({ flow: FLOW.P2P, servicingHubId: '' })).toEqual({
|
|
ordertype: FLOW.P2P,
|
|
pickup_source_type: PICKUP_SOURCE_TYPE.CUSTOMER
|
|
});
|
|
});
|
|
|
|
it('should fall back to Forward rather than put an invented type on the wire', () => {
|
|
expect(buildFlowFields({ flow: 'banana' }).ordertype).toBe(FLOW.FORWARD);
|
|
expect(buildFlowFields().ordertype).toBe(FLOW.FORWARD);
|
|
});
|
|
});
|
|
|
|
describe('pickup_source_type — what kind of place the parcel leaves', () => {
|
|
const baseAnchor = anchors.find((a) => a.kind === 'hub');
|
|
const siteAnchor = anchors.find((a) => a.kind === 'tenantlocation');
|
|
|
|
it('should name a base pickup and carry the base id', () => {
|
|
const fields = buildFlowFields({ flow: FLOW.FORWARD, pickupAnchor: baseAnchor });
|
|
expect(fields.pickup_source_type).toBe(PICKUP_SOURCE_TYPE.HUB);
|
|
expect(fields.pickuphubid).toBe(HUB.hubid);
|
|
});
|
|
|
|
it("should call a client's own site a merchant, not a base", () => {
|
|
// Both are facilities to the geometry, but a rider arriving at a kitchen
|
|
// is not arriving at a base — different signage, different person to ask
|
|
// for. Collapsing them is what made every logistics pickup read as the
|
|
// rider's own base.
|
|
const fields = buildFlowFields({ flow: FLOW.FORWARD, pickupAnchor: siteAnchor });
|
|
expect(fields.pickup_source_type).toBe(PICKUP_SOURCE_TYPE.MERCHANT);
|
|
expect(fields.pickuphubid).toBeUndefined();
|
|
});
|
|
|
|
it('should send "customer" as a value rather than leaving it off', () => {
|
|
// An omission reads as "nobody filled this in", which is a different fact
|
|
// from "this is somebody's front door".
|
|
expect(buildFlowFields({ flow: FLOW.P2P, pickupAnchor: null }).pickup_source_type).toBe(
|
|
PICKUP_SOURCE_TYPE.CUSTOMER
|
|
);
|
|
});
|
|
|
|
it('should never put an unknown source type on the wire', () => {
|
|
const fields = buildFlowFields({ flow: FLOW.FORWARD, pickupSourceType: 'banana', pickupAnchor: baseAnchor });
|
|
expect(fields.pickup_source_type).toBe(PICKUP_SOURCE_TYPE.HUB);
|
|
});
|
|
|
|
it('should not name a base on a pickup that is not from one', () => {
|
|
const fields = buildFlowFields({
|
|
flow: FLOW.REVERSE,
|
|
pickupSourceType: PICKUP_SOURCE_TYPE.CUSTOMER,
|
|
pickupAnchor: baseAnchor
|
|
});
|
|
expect(fields.pickuphubid).toBeUndefined();
|
|
});
|
|
|
|
it('should prefer the type stored on the booking over geometry', () => {
|
|
// A booking collected at a base whose coordinates were later edited must
|
|
// keep reading as a base pickup — the row recorded what actually happened.
|
|
const c = classifyBooking(
|
|
{
|
|
bookingid: 1,
|
|
pickup_source_type: PICKUP_SOURCE_TYPE.HUB,
|
|
pickuphubid: HUB.hubid,
|
|
pickuplatitude: DOOR_A.lat,
|
|
pickuplongitude: DOOR_A.lng,
|
|
deliverylatitude: DOOR_B.lat,
|
|
deliverylongitude: DOOR_B.lng
|
|
},
|
|
anchors
|
|
);
|
|
expect(c.pickupSourceType).toBe(PICKUP_SOURCE_TYPE.HUB);
|
|
expect(c.pickupHubId).toBe(HUB.hubid);
|
|
expect(c.sourceTypeFromServer).toBe(true);
|
|
});
|
|
|
|
it('should classify an older booking from geometry when the row says nothing', () => {
|
|
const c = classifyBooking(
|
|
{
|
|
bookingid: 2,
|
|
pickuplatitude: HUB.latitude,
|
|
pickuplongitude: HUB.longitude,
|
|
deliverylatitude: DOOR_B.lat,
|
|
deliverylongitude: DOOR_B.lng
|
|
},
|
|
anchors
|
|
);
|
|
expect(c.pickupSourceType).toBe(PICKUP_SOURCE_TYPE.HUB);
|
|
expect(c.sourceTypeFromServer).toBe(false);
|
|
});
|
|
});
|
|
});
|