diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/intercom/intercom-shell.svelte.test.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/intercom/intercom-shell.svelte.test.ts index 0b098ade40..dbeb4f03ea 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/intercom/intercom-shell.svelte.test.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/intercom/intercom-shell.svelte.test.ts @@ -1,11 +1,13 @@ import type { BootOptions } from 'svelte-intercom'; import { fireEvent, render, screen } from '@testing-library/svelte'; +import { tick } from 'svelte'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import IntercomShellTestHarness from './intercom-shell.test-harness.svelte'; const intercomShowMessages = vi.hoisted(() => vi.fn()); +const intercomUpdate = vi.hoisted(() => vi.fn()); vi.mock('$features/auth/index.svelte', () => ({ accessToken: { current: 'token_123' } @@ -33,12 +35,13 @@ vi.mock('@intercom/messenger-js-sdk', () => ({ startSurvey: vi.fn(), startTour: vi.fn(), trackEvent: vi.fn(), - update: vi.fn() + update: intercomUpdate })); describe('IntercomShell', () => { beforeEach(() => { intercomShowMessages.mockReset(); + intercomUpdate.mockReset(); vi.restoreAllMocks(); }); @@ -72,4 +75,38 @@ describe('IntercomShell', () => { expect(openWindow).toHaveBeenCalledTimes(1); expect(intercomShowMessages).toHaveBeenCalledTimes(1); }); + + it('remains stable across repeated tab visibility changes', async () => { + // Arrange + let hidden = false; + const addEventListener = vi.spyOn(document, 'addEventListener'); + vi.spyOn(document, 'hidden', 'get').mockImplementation(() => hidden); + const { rerender } = render(IntercomShellTestHarness, { + props: { + appId: 'app_123', + bootOptions: { intercomUserJwt: 'token_0', userId: 'user_123' } as BootOptions + } + }); + await tick(); + + // Act + for (let index = 0; index < 100; index++) { + hidden = true; + document.dispatchEvent(new Event('visibilitychange')); + await tick(); + + await rerender({ + appId: 'app_123', + bootOptions: { intercomUserJwt: `token_${index + 1}`, userId: 'user_123' } as BootOptions + }); + + hidden = false; + document.dispatchEvent(new Event('visibilitychange')); + await tick(); + } + + // Assert + expect(intercomUpdate).toHaveBeenCalled(); + expect(addEventListener.mock.calls.filter(([eventName]) => eventName === 'visibilitychange')).toHaveLength(1); + }); }); diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/document-visibility.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/document-visibility.svelte.ts index e44b84b5a1..e712b60a9c 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/document-visibility.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/document-visibility.svelte.ts @@ -2,30 +2,16 @@ import { useEventListener } from 'runed'; export class DocumentVisibility { get visible(): boolean { - if ($effect.tracking() && this.#effectRegistered === 0) { - // If we are in an effect and this effect has not been registered yet - // we match the current value, register the listener and return match - $effect(() => { - this.#effectRegistered++; - - useEventListener( - () => document, - 'visibilitychange', - () => (this.#visible = !document.hidden) - ); + return this.#visible; + } - return () => { - this.#effectRegistered--; - // if we deregister the event it means it's not used in any component - // and we want to go back to use the value from `this.#mediaQueryList.matches` - this.#visible = undefined; - }; - }); - } + #visible = $state(!document.hidden); - return this.#visible ?? !document.hidden; + constructor() { + useEventListener( + () => document, + 'visibilitychange', + () => (this.#visible = !document.hidden) + ); } - #effectRegistered = 0; - - #visible = $state(!document.hidden); }