import { describe, it, expect } from 'vitest'; import { parsePeriod, shapeStats } from '../services/statsShaping.js'; const MS_PER_DAY = 24 * 60 * 60 * 1000; // --------------------------------------------------------------------------- // parsePeriod — presets (autocomplete suggestions) // --------------------------------------------------------------------------- describe('parsePeriod — presets', () => { it('"7 days" → 7 days', () => { const r = parsePeriod('7 days'); expect(r.value).toBe(7); expect(r.unit).toBe('days'); expect(r.durationMs).toBe(7 * MS_PER_DAY); expect(r.label).toBe('7 days'); }); it('"30 days" → 30 days', () => { const r = parsePeriod('30 days'); expect(r.value).toBe(30); expect(r.unit).toBe('days'); expect(r.durationMs).toBe(30 * MS_PER_DAY); expect(r.label).toBe('30 days'); }); it('"3 months" → 3 × 30 days', () => { const r = parsePeriod('3 months'); expect(r.value).toBe(3); expect(r.unit).toBe('months'); expect(r.durationMs).toBe(3 * 30 * MS_PER_DAY); expect(r.label).toBe('3 months'); }); it('"6 months" → 6 × 30 days', () => { const r = parsePeriod('6 months'); expect(r.durationMs).toBe(6 * 30 * MS_PER_DAY); }); it('"1 year" → 365 days', () => { const r = parsePeriod('1 year'); expect(r.value).toBe(1); expect(r.unit).toBe('years'); expect(r.durationMs).toBe(365 * MS_PER_DAY); expect(r.label).toBe('1 year'); }); }); // --------------------------------------------------------------------------- // parsePeriod — day unit variants // --------------------------------------------------------------------------- describe('parsePeriod — day variants', () => { it('d', () => { const r = parsePeriod('14d'); expect(r.value).toBe(14); expect(r.unit).toBe('days'); expect(r.durationMs).toBe(14 * MS_PER_DAY); }); it('day (singular, no space)', () => { const r = parsePeriod('1day'); expect(r.value).toBe(1); expect(r.unit).toBe('days'); expect(r.label).toBe('1 day'); }); it(' day (singular, with space)', () => { const r = parsePeriod('1 day'); expect(r.value).toBe(1); expect(r.label).toBe('1 day'); }); it(' days', () => { const r = parsePeriod('10 days'); expect(r.durationMs).toBe(10 * MS_PER_DAY); }); }); // --------------------------------------------------------------------------- // parsePeriod — week unit variants // --------------------------------------------------------------------------- describe('parsePeriod — week variants', () => { it('w', () => { const r = parsePeriod('2w'); expect(r.value).toBe(2); expect(r.unit).toBe('weeks'); expect(r.durationMs).toBe(2 * 7 * MS_PER_DAY); expect(r.label).toBe('2 weeks'); }); it(' week (singular)', () => { const r = parsePeriod('1 week'); expect(r.value).toBe(1); expect(r.label).toBe('1 week'); }); it('weeks (no space)', () => { const r = parsePeriod('4weeks'); expect(r.value).toBe(4); expect(r.unit).toBe('weeks'); }); it(' weeks', () => { const r = parsePeriod('4 weeks'); expect(r.durationMs).toBe(4 * 7 * MS_PER_DAY); }); }); // --------------------------------------------------------------------------- // parsePeriod — month unit variants // --------------------------------------------------------------------------- describe('parsePeriod — month variants', () => { it('m', () => { const r = parsePeriod('3m'); expect(r.unit).toBe('months'); expect(r.durationMs).toBe(3 * 30 * MS_PER_DAY); }); it('mo', () => { const r = parsePeriod('6mo'); expect(r.unit).toBe('months'); expect(r.durationMs).toBe(6 * 30 * MS_PER_DAY); }); it(' month (singular)', () => { const r = parsePeriod('1 month'); expect(r.value).toBe(1); expect(r.label).toBe('1 month'); }); it(' months', () => { const r = parsePeriod('12 months'); expect(r.value).toBe(12); expect(r.unit).toBe('months'); expect(r.durationMs).toBe(12 * 30 * MS_PER_DAY); }); }); // --------------------------------------------------------------------------- // parsePeriod — year unit variants // --------------------------------------------------------------------------- describe('parsePeriod — year variants', () => { it('y', () => { const r = parsePeriod('1y'); expect(r.unit).toBe('years'); expect(r.durationMs).toBe(365 * MS_PER_DAY); }); it(' year (singular)', () => { const r = parsePeriod('1 year'); expect(r.unit).toBe('years'); expect(r.label).toBe('1 year'); }); it(' years', () => { const r = parsePeriod('2 years'); expect(r.value).toBe(2); expect(r.label).toBe('2 years'); expect(r.durationMs).toBe(2 * 365 * MS_PER_DAY); }); }); // --------------------------------------------------------------------------- // parsePeriod — bare number = days // --------------------------------------------------------------------------- describe('parsePeriod — bare number defaults to days', () => { it('"30" → 30 days', () => { const r = parsePeriod('30'); expect(r.value).toBe(30); expect(r.unit).toBe('days'); expect(r.durationMs).toBe(30 * MS_PER_DAY); }); it('"7" → 7 days', () => { const r = parsePeriod('7'); expect(r.unit).toBe('days'); expect(r.durationMs).toBe(7 * MS_PER_DAY); }); it('"365" → 365 days (not 1 year)', () => { const r = parsePeriod('365'); expect(r.unit).toBe('days'); expect(r.value).toBe(365); }); }); // --------------------------------------------------------------------------- // parsePeriod — case & whitespace tolerance // --------------------------------------------------------------------------- describe('parsePeriod — case and whitespace tolerance', () => { it('uppercase "7 DAYS"', () => { const r = parsePeriod('7 DAYS'); expect(r.value).toBe(7); expect(r.unit).toBe('days'); }); it('mixed case "3 Months"', () => { const r = parsePeriod('3 Months'); expect(r.unit).toBe('months'); }); it('leading and trailing whitespace " 30 days "', () => { const r = parsePeriod(' 30 days '); expect(r.value).toBe(30); expect(r.unit).toBe('days'); }); it('multiple internal spaces "7 days"', () => { const r = parsePeriod('7 days'); expect(r.value).toBe(7); expect(r.unit).toBe('days'); }); it('no space between number and unit "30days"', () => { const r = parsePeriod('30days'); expect(r.value).toBe(30); expect(r.unit).toBe('days'); }); it('"1YEAR" (uppercase, no space)', () => { const r = parsePeriod('1YEAR'); expect(r.unit).toBe('years'); }); }); // --------------------------------------------------------------------------- // parsePeriod — unparseable → 30-day default // --------------------------------------------------------------------------- describe('parsePeriod — unparseable inputs → 30-day default', () => { const expectDefault = r => { expect(r.value).toBe(30); expect(r.unit).toBe('days'); expect(r.durationMs).toBe(30 * MS_PER_DAY); expect(r.label).toBe('30 days'); }; it('null → default', () => expectDefault(parsePeriod(null))); it('undefined → default', () => expectDefault(parsePeriod(undefined))); it('empty string → default', () => expectDefault(parsePeriod(''))); it('whitespace only → default', () => expectDefault(parsePeriod(' '))); it('letters only → default', () => expectDefault(parsePeriod('abc'))); it('natural language → default', () => expectDefault(parsePeriod('last month'))); it('unknown unit "5x" → default', () => expectDefault(parsePeriod('5x'))); it('"0" → default (zero is nonsensical)', () => expectDefault(parsePeriod('0'))); it('"0d" → default', () => expectDefault(parsePeriod('0d'))); it('negative-like "-5d" → default (not a digit-start)', () => expectDefault(parsePeriod('-5d'))); }); // --------------------------------------------------------------------------- // parsePeriod — return shape invariant // --------------------------------------------------------------------------- describe('parsePeriod — return shape', () => { it('always returns { durationMs, value, unit, label }', () => { for (const input of ['7d', '2w', '3m', '6mo', '1y', '30', null, 'junk']) { const r = parsePeriod(input); expect(typeof r.durationMs).toBe('number'); expect(typeof r.value).toBe('number'); expect(typeof r.unit).toBe('string'); expect(typeof r.label).toBe('string'); } }); it('returns a fresh object each call (not the same frozen reference)', () => { const a = parsePeriod(null); const b = parsePeriod(undefined); expect(a).not.toBe(b); }); }); // =========================================================================== // shapeStats — fixtures // =========================================================================== const MEMBER = 'member-001'; const OTHER = 'other-002'; function event(overrides) { return { staffId: OTHER, type: 'claim', tier: 0, ticketType: 'email', wasClaimed: null, resolverId: null, fromId: null, toId: null, ...overrides }; } // --------------------------------------------------------------------------- // shapeStats — claims // --------------------------------------------------------------------------- describe('shapeStats — claims', () => { it('counts claim events where staffId===member', () => { const events = [ event({ type: 'claim', staffId: MEMBER }), event({ type: 'claim', staffId: MEMBER }), event({ type: 'claim', staffId: OTHER }), ]; expect(shapeStats(events, MEMBER, 'all').claims).toBe(2); }); it('does not count claims by other staff', () => { expect(shapeStats([event({ type: 'claim', staffId: OTHER })], MEMBER, 'all').claims).toBe(0); }); it('non-claim event types do not increment claims', () => { const events = [ event({ type: 'close', staffId: MEMBER }), event({ type: 'escalate', staffId: MEMBER }), ]; expect(shapeStats(events, MEMBER, 'all').claims).toBe(0); }); it('claimsWhileEscalated groups by numeric tier for tier > 0', () => { const events = [ event({ type: 'claim', staffId: MEMBER, tier: 0 }), event({ type: 'claim', staffId: MEMBER, tier: 1 }), event({ type: 'claim', staffId: MEMBER, tier: 1 }), event({ type: 'claim', staffId: MEMBER, tier: 2 }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.claims).toBe(4); expect(r.claimsWhileEscalated).toEqual({ 1: 2, 2: 1 }); }); it('tier=0 claims are NOT included in claimsWhileEscalated', () => { const events = [event({ type: 'claim', staffId: MEMBER, tier: 0 })]; expect(shapeStats(events, MEMBER, 'all').claimsWhileEscalated).toEqual({}); }); it('claimsWhileEscalated only includes the member\'s own claims', () => { const events = [event({ type: 'claim', staffId: OTHER, tier: 1 })]; expect(shapeStats(events, MEMBER, 'all').claimsWhileEscalated).toEqual({}); }); }); // --------------------------------------------------------------------------- // shapeStats — closes // --------------------------------------------------------------------------- describe('shapeStats — closes', () => { it('counts close events where staffId===member', () => { const events = [ event({ type: 'close', staffId: MEMBER }), event({ type: 'close', staffId: MEMBER }), event({ type: 'close', staffId: OTHER }), event({ type: 'close', staffId: 'system' }), ]; expect(shapeStats(events, MEMBER, 'all').closes).toBe(2); }); it('system closes do not count toward member closes', () => { const events = [event({ type: 'close', staffId: 'system', resolverId: MEMBER })]; expect(shapeStats(events, MEMBER, 'all').closes).toBe(0); }); it('unclaimedAtClose counts member closes where wasClaimed===false', () => { const events = [ event({ type: 'close', staffId: MEMBER, wasClaimed: false }), event({ type: 'close', staffId: MEMBER, wasClaimed: true }), event({ type: 'close', staffId: MEMBER, wasClaimed: false }), event({ type: 'close', staffId: OTHER, wasClaimed: false }), ]; expect(shapeStats(events, MEMBER, 'all').unclaimedAtClose).toBe(2); }); it('wasClaimed===true does NOT count as unclaimed-at-close', () => { const events = [event({ type: 'close', staffId: MEMBER, wasClaimed: true })]; expect(shapeStats(events, MEMBER, 'all').unclaimedAtClose).toBe(0); }); it('wasClaimed===null does NOT count as unclaimed-at-close', () => { const events = [event({ type: 'close', staffId: MEMBER, wasClaimed: null })]; expect(shapeStats(events, MEMBER, 'all').unclaimedAtClose).toBe(0); }); }); // --------------------------------------------------------------------------- // shapeStats — resolved (credit to claimer via resolverId) // --------------------------------------------------------------------------- describe('shapeStats — resolved', () => { it('counts close events where resolverId===member', () => { const events = [ event({ type: 'close', staffId: OTHER, resolverId: MEMBER }), event({ type: 'close', staffId: OTHER, resolverId: OTHER }), event({ type: 'close', staffId: MEMBER, resolverId: MEMBER }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.resolved).toBe(2); expect(r.closes).toBe(1); }); it('resolved is distinct from closes — different field keys', () => { const events = [ event({ type: 'close', staffId: OTHER, resolverId: MEMBER }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.resolved).toBe(1); expect(r.closes).toBe(0); }); it('a self-close-and-resolve increments both closes and resolved', () => { const events = [ event({ type: 'close', staffId: MEMBER, resolverId: MEMBER, wasClaimed: true }) ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.closes).toBe(1); expect(r.resolved).toBe(1); }); }); // --------------------------------------------------------------------------- // shapeStats — escalations / de-escalations // --------------------------------------------------------------------------- describe('shapeStats — escalations', () => { it('groups escalate events by numeric tier', () => { const events = [ event({ type: 'escalate', staffId: MEMBER, tier: 1 }), event({ type: 'escalate', staffId: MEMBER, tier: 1 }), event({ type: 'escalate', staffId: MEMBER, tier: 2 }), ]; expect(shapeStats(events, MEMBER, 'all').escalations).toEqual({ 1: 2, 2: 1 }); }); it('ignores escalations by other staff', () => { expect(shapeStats([event({ type: 'escalate', staffId: OTHER, tier: 1 })], MEMBER, 'all').escalations).toEqual({}); }); it('escalations is empty when no escalate events', () => { expect(shapeStats([event({ type: 'claim', staffId: MEMBER })], MEMBER, 'all').escalations).toEqual({}); }); }); describe('shapeStats — de-escalations', () => { it('groups deescalate events by numeric tier', () => { const events = [ event({ type: 'deescalate', staffId: MEMBER, tier: 1 }), event({ type: 'deescalate', staffId: MEMBER, tier: 2 }), event({ type: 'deescalate', staffId: MEMBER, tier: 2 }), ]; expect(shapeStats(events, MEMBER, 'all').deescalations).toEqual({ 1: 1, 2: 2 }); }); it('ignores deescalations by other staff', () => { expect(shapeStats([event({ type: 'deescalate', staffId: OTHER, tier: 1 })], MEMBER, 'all').deescalations).toEqual({}); }); it('escalations and deescalations are counted independently', () => { const events = [ event({ type: 'escalate', staffId: MEMBER, tier: 1 }), event({ type: 'deescalate', staffId: MEMBER, tier: 1 }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.escalations).toEqual({ 1: 1 }); expect(r.deescalations).toEqual({ 1: 1 }); }); }); // --------------------------------------------------------------------------- // shapeStats — transfers in vs out // --------------------------------------------------------------------------- describe('shapeStats — transfers', () => { it('transfersIn counts transfer events where toId===member', () => { const events = [ event({ type: 'transfer', staffId: OTHER, toId: MEMBER }), event({ type: 'transfer', staffId: OTHER, toId: OTHER }), ]; expect(shapeStats(events, MEMBER, 'all').transfersIn).toBe(1); }); it('transfersOut counts transfer events where staffId===member (initiator)', () => { const events = [ event({ type: 'transfer', staffId: MEMBER, toId: OTHER }), event({ type: 'transfer', staffId: OTHER, toId: OTHER }), ]; expect(shapeStats(events, MEMBER, 'all').transfersOut).toBe(1); }); it('single transfer counts out for sender, in for receiver', () => { const events = [event({ type: 'transfer', staffId: MEMBER, toId: OTHER })]; const rMember = shapeStats(events, MEMBER, 'all'); const rOther = shapeStats(events, OTHER, 'all'); expect(rMember.transfersOut).toBe(1); expect(rMember.transfersIn).toBe(0); expect(rOther.transfersIn).toBe(1); expect(rOther.transfersOut).toBe(0); }); it('transfersIn and transfersOut are counted on a single event if member is both', () => { // Degenerate: staffId===toId===member. Phase 5b prevents this in practice, // but the shaper is pure and should still count both dimensions. const events = [event({ type: 'transfer', staffId: MEMBER, toId: MEMBER })]; const r = shapeStats(events, MEMBER, 'all'); expect(r.transfersOut).toBe(1); expect(r.transfersIn).toBe(1); }); }); // --------------------------------------------------------------------------- // shapeStats — reopens (via resolverId, not staffId) // --------------------------------------------------------------------------- describe('shapeStats — reopens', () => { it('counts reopen events where resolverId===member', () => { const events = [ event({ type: 'reopen', staffId: 'system', resolverId: MEMBER }), event({ type: 'reopen', staffId: 'system', resolverId: OTHER }), ]; expect(shapeStats(events, MEMBER, 'all').reopens).toBe(1); }); it('staffId on reopen is typically "system" — does not drive the reopen count', () => { const events = [event({ type: 'reopen', staffId: MEMBER, resolverId: OTHER })]; expect(shapeStats(events, MEMBER, 'all').reopens).toBe(0); }); it('null resolverId does not count', () => { const events = [event({ type: 'reopen', staffId: 'system', resolverId: null })]; expect(shapeStats(events, MEMBER, 'all').reopens).toBe(0); }); }); // --------------------------------------------------------------------------- // shapeStats — source filter // --------------------------------------------------------------------------- describe('shapeStats — source filter', () => { it('"all" includes both email and discord events', () => { const events = [ event({ type: 'claim', staffId: MEMBER, ticketType: 'email' }), event({ type: 'claim', staffId: MEMBER, ticketType: 'discord' }), ]; expect(shapeStats(events, MEMBER, 'all').claims).toBe(2); }); it('"email" includes only email events', () => { const events = [ event({ type: 'claim', staffId: MEMBER, ticketType: 'email' }), event({ type: 'claim', staffId: MEMBER, ticketType: 'discord' }), ]; expect(shapeStats(events, MEMBER, 'email').claims).toBe(1); }); it('"discord" includes only discord events', () => { const events = [ event({ type: 'claim', staffId: MEMBER, ticketType: 'email' }), event({ type: 'claim', staffId: MEMBER, ticketType: 'discord' }), ]; expect(shapeStats(events, MEMBER, 'discord').claims).toBe(1); }); it('source filter applies before all metric calculations', () => { const events = [ event({ type: 'close', staffId: MEMBER, resolverId: MEMBER, wasClaimed: false, ticketType: 'email' }), event({ type: 'close', staffId: MEMBER, resolverId: MEMBER, wasClaimed: true, ticketType: 'discord' }), ]; const r = shapeStats(events, MEMBER, 'email'); expect(r.closes).toBe(1); expect(r.resolved).toBe(1); expect(r.unclaimedAtClose).toBe(1); }); it('undefined source defaults to "all"', () => { const events = [ event({ type: 'claim', staffId: MEMBER, ticketType: 'email' }), event({ type: 'claim', staffId: MEMBER, ticketType: 'discord' }), ]; expect(shapeStats(events, MEMBER).claims).toBe(2); }); }); // --------------------------------------------------------------------------- // shapeStats — bySource breakdown // --------------------------------------------------------------------------- describe('shapeStats — bySource breakdown', () => { it('splits claims by email/discord', () => { const events = [ event({ type: 'claim', staffId: MEMBER, ticketType: 'email' }), event({ type: 'claim', staffId: MEMBER, ticketType: 'discord' }), event({ type: 'claim', staffId: MEMBER, ticketType: 'discord' }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.bySource.email.claims).toBe(1); expect(r.bySource.discord.claims).toBe(2); }); it('splits closes by email/discord', () => { const events = [ event({ type: 'close', staffId: MEMBER, ticketType: 'email' }), event({ type: 'close', staffId: MEMBER, ticketType: 'discord' }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.bySource.email.closes).toBe(1); expect(r.bySource.discord.closes).toBe(1); }); it('splits resolved by email/discord (using resolverId key)', () => { const events = [ event({ type: 'close', staffId: OTHER, resolverId: MEMBER, ticketType: 'email' }), event({ type: 'close', staffId: OTHER, resolverId: MEMBER, ticketType: 'discord' }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.bySource.email.resolved).toBe(1); expect(r.bySource.discord.resolved).toBe(1); }); it('events with unknown ticketType are bucketed as email', () => { const events = [event({ type: 'claim', staffId: MEMBER, ticketType: undefined })]; const r = shapeStats(events, MEMBER, 'all'); expect(r.bySource.email.claims).toBe(1); expect(r.bySource.discord.claims).toBe(0); }); it('bySource totals match headline counts', () => { const events = [ event({ type: 'claim', staffId: MEMBER, ticketType: 'email' }), event({ type: 'claim', staffId: MEMBER, ticketType: 'discord' }), event({ type: 'close', staffId: MEMBER, resolverId: MEMBER, wasClaimed: true, ticketType: 'email' }), event({ type: 'close', staffId: OTHER, resolverId: MEMBER, ticketType: 'discord' }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.bySource.email.claims + r.bySource.discord.claims).toBe(r.claims); expect(r.bySource.email.closes + r.bySource.discord.closes).toBe(r.closes); expect(r.bySource.email.resolved + r.bySource.discord.resolved).toBe(r.resolved); }); }); // --------------------------------------------------------------------------- // shapeStats — edge cases // --------------------------------------------------------------------------- describe('shapeStats — edge cases', () => { it('empty events array returns zero counts', () => { const r = shapeStats([], MEMBER, 'all'); expect(r.claims).toBe(0); expect(r.closes).toBe(0); expect(r.resolved).toBe(0); expect(r.unclaimedAtClose).toBe(0); expect(r.transfersIn).toBe(0); expect(r.transfersOut).toBe(0); expect(r.reopens).toBe(0); expect(r.claimsWhileEscalated).toEqual({}); expect(r.escalations).toEqual({}); expect(r.deescalations).toEqual({}); expect(r.bySource.email.claims).toBe(0); expect(r.bySource.discord.claims).toBe(0); }); it('null events array is treated as empty', () => { const r = shapeStats(null, MEMBER, 'all'); expect(r.claims).toBe(0); }); it('events from other members are ignored for the requested member', () => { const events = [ event({ type: 'claim', staffId: OTHER }), event({ type: 'close', staffId: OTHER, resolverId: OTHER }), event({ type: 'transfer', staffId: OTHER, toId: OTHER }), event({ type: 'reopen', staffId: 'system', resolverId: OTHER }), event({ type: 'escalate', staffId: OTHER, tier: 1 }), event({ type: 'deescalate',staffId: OTHER, tier: 1 }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.claims).toBe(0); expect(r.closes).toBe(0); expect(r.resolved).toBe(0); expect(r.transfersIn).toBe(0); expect(r.transfersOut).toBe(0); expect(r.reopens).toBe(0); expect(r.escalations).toEqual({}); expect(r.deescalations).toEqual({}); }); it('handles member appearing in multiple roles across events', () => { const events = [ event({ type: 'claim', staffId: MEMBER, tier: 0, ticketType: 'email' }), event({ type: 'close', staffId: MEMBER, resolverId: OTHER, wasClaimed: false, ticketType: 'email' }), event({ type: 'close', staffId: OTHER, resolverId: MEMBER, wasClaimed: true, ticketType: 'discord' }), event({ type: 'transfer', staffId: OTHER, toId: MEMBER }), event({ type: 'reopen', staffId: 'system', resolverId: MEMBER }), event({ type: 'escalate', staffId: MEMBER, tier: 1 }), ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.claims).toBe(1); expect(r.closes).toBe(1); expect(r.unclaimedAtClose).toBe(1); expect(r.resolved).toBe(1); expect(r.transfersIn).toBe(1); expect(r.transfersOut).toBe(0); expect(r.reopens).toBe(1); expect(r.escalations).toEqual({ 1: 1 }); }); it('events matching no member fields contribute nothing', () => { const events = [ event({ type: 'response', staffId: MEMBER }), // 'response' type has no shaper rule ]; const r = shapeStats(events, MEMBER, 'all'); expect(r.claims + r.closes + r.resolved + r.transfersIn + r.transfersOut + r.reopens).toBe(0); }); });