HEX
Server: LiteSpeed
System: Linux server342.web-hosting.com 4.18.0-553.124.4.lve.el8.x86_64 #1 SMP Fri May 15 13:02:13 UTC 2026 x86_64
User: ksonpoau (1099)
PHP: 8.2.31
Disabled: NONE
Upload Files
File: //proc/self/cwd/wp-content/plugins/extendify/tests/unit/QuickEdit/lib/insights.test.js
// Pins the no-PII payload contract per the plan's "no PII to backends"
// hard rule, plus the keepalive + swallow-errors posture and the
// short-circuits (missing event, missing INSIGHTS_HOST).

const mockFetch = jest.fn();

jest.mock('@constants', () => ({
	INSIGHTS_HOST: 'https://insights.test',
	AI_HOST: '',
	PATTERNS_HOST: '',
	IMAGES_HOST: '',
	KB_HOST: '',
}));

jest.mock('@shared/lib/data', () => ({
	reqDataBasics: {
		partnerId: 'p1',
		siteId: 's1',
		version: '1.0.0',
		homeUrl: 'https://my-site.test',
	},
}));

beforeEach(() => {
	jest.resetModules();
	jest.clearAllMocks();
	global.fetch = mockFetch;
	mockFetch.mockReset();
	mockFetch.mockReturnValue({ catch: () => {} });
});

afterEach(() => {
	delete global.fetch;
});

describe('track — request shape', () => {
	it('POSTs to INSIGHTS_HOST + /api/v1/quick-edit with JSON + Extendify headers + keepalive', async () => {
		const { track } = await import('@quick-edit/lib/insights');
		track('edit_mode_on', { source: 'admin-bar' });

		expect(mockFetch).toHaveBeenCalledTimes(1);
		const [url, init] = mockFetch.mock.calls[0];
		expect(url).toBe('https://insights.test/api/v1/quick-edit');
		expect(init.method).toBe('POST');
		expect(init.keepalive).toBe(true);
		expect(init.headers).toEqual({
			'Content-type': 'application/json',
			Accept: 'application/json',
			'X-Extendify': 'true',
		});
	});
});

describe('track — payload shape (no PII)', () => {
	it('includes only reqDataBasics + event + props + ts', async () => {
		const { track } = await import('@quick-edit/lib/insights');
		track('edit_mode_on', { source: 'admin-bar' });
		const body = JSON.parse(mockFetch.mock.calls[0][1].body);

		expect(Object.keys(body).sort()).toEqual(
			[
				'event',
				'homeUrl',
				'partnerId',
				'props',
				'siteId',
				'ts',
				'version',
			].sort(),
		);
		expect(body.event).toBe('edit_mode_on');
		expect(body.props).toEqual({ source: 'admin-bar' });
		expect(typeof body.ts).toBe('number');
	});

	it('defaults props to an empty object when omitted', async () => {
		const { track } = await import('@quick-edit/lib/insights');
		track('edit_mode_on');
		const body = JSON.parse(mockFetch.mock.calls[0][1].body);
		expect(body.props).toEqual({});
	});

	it('forwards the props object verbatim (no field filtering at the helper level)', async () => {
		// Characterization: track() does not sanitize props itself — call sites
		// are responsible for not putting PII into the props bag. If you change
		// this to enforce a deny-list, update this test.
		const { track } = await import('@quick-edit/lib/insights');
		track('edit_mode_on', { foo: 'bar', n: 42 });
		const body = JSON.parse(mockFetch.mock.calls[0][1].body);
		expect(body.props).toEqual({ foo: 'bar', n: 42 });
	});
});

describe('track — short-circuits', () => {
	it('does not fire fetch when event is empty', async () => {
		const { track } = await import('@quick-edit/lib/insights');
		track('');
		track(null);
		track(undefined);
		expect(mockFetch).not.toHaveBeenCalled();
	});

	it('does not fire fetch when INSIGHTS_HOST is falsy', async () => {
		jest.resetModules();
		jest.doMock('@constants', () => ({
			INSIGHTS_HOST: '',
			AI_HOST: '',
			PATTERNS_HOST: '',
			IMAGES_HOST: '',
			KB_HOST: '',
		}));
		const { track } = await import('@quick-edit/lib/insights');
		track('edit_mode_on');
		expect(mockFetch).not.toHaveBeenCalled();
	});
});

describe('track — error swallowing', () => {
	it('swallows a synchronous throw inside fetch', async () => {
		mockFetch.mockImplementation(() => {
			throw new Error('synchronous boom');
		});
		const { track } = await import('@quick-edit/lib/insights');
		expect(() => track('edit_mode_on')).not.toThrow();
	});

	it('swallows an async fetch rejection (no unhandled rejection)', async () => {
		// Create the rejected promise inside the mock impl so .catch()
		// attaches in the same microtask — otherwise the rejection would
		// surface as unhandled before the dynamic import returns.
		mockFetch.mockImplementation(() => Promise.reject(new Error('async boom')));
		const { track } = await import('@quick-edit/lib/insights');
		expect(() => track('edit_mode_on')).not.toThrow();
		await Promise.resolve();
	});
});