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/components/InlineEditor.test.jsx
// InlineEditor is the orchestrator: it reads `selected` from the
// QuickEdit store and dispatches to one of three branches —
// (a) text blocks → BlockTextEditor,
// (b) image blocks → ImagePicker (inline menu),
// (c) modal block types → mountModal(<TheRightModal …/>).
// Tests pin the routing decision per block type, the props handed to each
// modal, and the onAfterSave reload-on-true contract.

import { render } from '@testing-library/react';

const mockMountModal = jest.fn();
const mockCloseModal = jest.fn();
const mockClearSelected = jest.fn();
let mockSelected = null;

jest.mock('@quick-edit/lib/api', () => ({
	loadProduct: jest.fn(),
	save: jest.fn(),
	saveProduct: jest.fn(),
}));
jest.mock('@quick-edit/lib/block-source-cache', () => ({
	invalidateBlockSource: jest.fn(),
}));
jest.mock('@quick-edit/lib/dom', () => ({
	splice: jest.fn(),
}));
jest.mock('@quick-edit/lib/insights', () => ({
	track: jest.fn(),
}));
jest.mock('@quick-edit/lib/modal-root', () => ({
	mountModal: (...args) => mockMountModal(...args),
	closeModal: (...args) => mockCloseModal(...args),
}));
jest.mock('@quick-edit/state/store', () => ({
	useQuickEditStore: (selector) =>
		selector({ selected: mockSelected, clearSelected: mockClearSelected }),
}));
jest.mock('@quick-edit/state/undo', () => ({
	pushUndo: jest.fn(),
}));

jest.mock('@quick-edit/components/BlockTextEditor', () => ({
	BlockTextEditor: (props) => (
		<div
			data-testid="block-text-editor"
			data-block-type={props.selected.blockType}
		/>
	),
}));

const stubModal = (name) => (props) => (
	<div
		data-testid={name}
		data-props={JSON.stringify(
			Object.fromEntries(
				Object.entries(props).filter(([, v]) => typeof v !== 'function'),
			),
		)}
	/>
);

jest.mock('@quick-edit/components/modals/AiImagePickerModal', () => ({
	AiImagePickerModal: stubModal('AiImagePickerModal'),
}));
jest.mock('@quick-edit/components/modals/NavItemModal', () => ({
	NavItemModal: stubModal('NavItemModal'),
}));
jest.mock('@quick-edit/components/modals/ProductPriceModal', () => ({
	ProductPriceModal: stubModal('ProductPriceModal'),
}));
jest.mock('@quick-edit/components/modals/ProductTextModal', () => ({
	ProductTextModal: stubModal('ProductTextModal'),
}));
jest.mock('@quick-edit/components/modals/SiteIdentityModal', () => ({
	SiteIdentityModal: stubModal('SiteIdentityModal'),
}));
jest.mock('@quick-edit/components/modals/SocialLinkModal', () => ({
	SocialLinkModal: stubModal('SocialLinkModal'),
}));
jest.mock('@quick-edit/components/modals/UnsplashImagePickerModal', () => ({
	UnsplashImagePickerModal: stubModal('UnsplashImagePickerModal'),
}));
jest.mock('@quick-edit/components/modals/WPFormsFieldModal', () => ({
	WPFormsFieldModal: stubModal('WPFormsFieldModal'),
}));

beforeEach(() => {
	jest.clearAllMocks();
	mockSelected = null;
	document.body.innerHTML = '';
});

const importComponent = () => require('@quick-edit/components/InlineEditor');

const renderWithSelected = (sel) => {
	mockSelected = sel;
	const { InlineEditor } = importComponent();
	return render(<InlineEditor />);
};

const mountedElement = () => {
	expect(mockMountModal).toHaveBeenCalledTimes(1);
	return mockMountModal.mock.calls[0][0];
};

describe('InlineEditor — null selection', () => {
	it('renders nothing and mounts no modal when selected is null', () => {
		const { container } = renderWithSelected(null);
		expect(container.firstChild).toBeNull();
		expect(mockMountModal).not.toHaveBeenCalled();
	});
});

describe('InlineEditor — text routing (TEXT_STRATEGIES)', () => {
	const cases = ['core/paragraph', 'core/heading', 'core/button'];
	for (const blockType of cases) {
		it(`renders BlockTextEditor for ${blockType}`, () => {
			const { getByTestId } = renderWithSelected({
				blockType,
				blockId: 'b-1',
				el: document.createElement('p'),
				source: { kind: 'post', id: 1 },
			});
			expect(
				getByTestId('block-text-editor').getAttribute('data-block-type'),
			).toBe(blockType);
			expect(mockMountModal).not.toHaveBeenCalled();
		});
	}
});

describe('InlineEditor — image routing (PICKER_STRATEGIES)', () => {
	const cases = ['core/image', 'core/cover', 'product:image'];
	for (const blockType of cases) {
		it(`renders the ImagePicker menu for ${blockType} (no modal mount)`, () => {
			renderWithSelected({
				blockType,
				blockId: 'b-img',
				el: document.createElement('div'),
				source: { kind: 'post', id: 1 },
			});
			expect(
				document.getElementById('extendify-quick-edit-image-menu'),
			).not.toBeNull();
			expect(mockMountModal).not.toHaveBeenCalled();
		});
	}
});

describe('InlineEditor — image menu drops from the hover bar', () => {
	// positionBar (lib/hover-bar.js) places the pill above OR below the image
	// depending on viewport room and leaves it mounted for picker blocks. The
	// menu must follow the pill, not assume it sits at the image's top edge.
	const rect = (top, bottom, left = 400, width = 200) => ({
		top,
		bottom,
		left,
		right: left + width,
		width,
		height: bottom - top,
	});
	const imageEl = (r) => {
		const el = document.createElement('div');
		el.getBoundingClientRect = () => r;
		return el;
	};
	const mountBar = (r) => {
		const bar = document.createElement('div');
		bar.className = 'extendify-quick-edit-bar';
		bar.getBoundingClientRect = () => r;
		document.body.appendChild(bar);
	};

	beforeEach(() => {
		Object.defineProperty(window, 'innerWidth', {
			value: 1280,
			configurable: true,
		});
		Object.defineProperty(window, 'innerHeight', {
			value: 900,
			configurable: true,
		});
	});

	it('drops below the pill when the bar sits below the image', () => {
		mountBar(rect(608, 644)); // pill pushed below a top-anchored image
		renderWithSelected({
			blockType: 'core/image',
			blockId: 'b-below',
			el: imageEl(rect(40, 600, 300, 600)),
			source: { kind: 'post', id: 1 },
		});
		const menu = document.getElementById('extendify-quick-edit-image-menu');
		// bar.bottom (644) + GAP (6) — NOT the image's top edge (40).
		expect(menu.style.top).toBe('650px');
	});

	it('drops just under the pill when the bar sits above the image', () => {
		mountBar(rect(256, 292));
		renderWithSelected({
			blockType: 'core/image',
			blockId: 'b-above',
			el: imageEl(rect(300, 700, 300, 600)),
			source: { kind: 'post', id: 1 },
		});
		const menu = document.getElementById('extendify-quick-edit-image-menu');
		expect(menu.style.top).toBe('298px'); // 292 + 6
	});

	it('falls back to the image top edge when no bar is mounted', () => {
		renderWithSelected({
			blockType: 'core/image',
			blockId: 'b-nobar',
			el: imageEl(rect(120, 520, 300, 600)),
			source: { kind: 'post', id: 1 },
		});
		const menu = document.getElementById('extendify-quick-edit-image-menu');
		expect(menu.style.top).toBe('120px');
	});
});

describe('InlineEditor — modal routing (MODAL_BLOCK_TYPES)', () => {
	it('site-title → SiteIdentityModal with kind="title"', () => {
		renderWithSelected({ blockType: 'core/site-title' });
		const el = mountedElement();
		expect(el.props.kind).toBe('title');
	});

	it('site-tagline → SiteIdentityModal with kind="tagline"', () => {
		renderWithSelected({ blockType: 'core/site-tagline' });
		const el = mountedElement();
		expect(el.props.kind).toBe('tagline');
	});

	it('site-logo → SiteIdentityModal with kind="logo"', () => {
		renderWithSelected({ blockType: 'core/site-logo' });
		const el = mountedElement();
		expect(el.props.kind).toBe('logo');
	});

	it('social-link → SocialLinkModal carrying the selection', () => {
		const sel = { blockType: 'core/social-link', blockId: 'sl-1' };
		renderWithSelected(sel);
		const el = mountedElement();
		expect(el.props.selected).toBe(sel);
	});

	it('navigation-link → NavItemModal', () => {
		const sel = { blockType: 'core/navigation-link', blockId: 'n-1' };
		renderWithSelected(sel);
		const el = mountedElement();
		expect(el.props.selected).toBe(sel);
	});

	it('navigation-submenu also routes to NavItemModal', () => {
		const sel = { blockType: 'core/navigation-submenu', blockId: 'n-2' };
		renderWithSelected(sel);
		const el = mountedElement();
		expect(el.props.selected).toBe(sel);
	});

	it('product:name → ProductTextModal with productId + productField', () => {
		renderWithSelected({
			blockType: 'product:name',
			productId: 99,
			productField: 'name',
		});
		const el = mountedElement();
		expect(el.props.productId).toBe(99);
		expect(el.props.field).toBe('name');
	});

	it('product:short_description → ProductTextModal with productId + productField', () => {
		renderWithSelected({
			blockType: 'product:short_description',
			productId: 99,
			productField: 'short_description',
		});
		const el = mountedElement();
		expect(el.props.productId).toBe(99);
		expect(el.props.field).toBe('short_description');
	});

	it('product:price → ProductPriceModal with productId', () => {
		renderWithSelected({ blockType: 'product:price', productId: 7 });
		const el = mountedElement();
		expect(el.props.productId).toBe(7);
	});

	it('wpforms:field → WPFormsFieldModal with formId + fieldId', () => {
		renderWithSelected({
			blockType: 'wpforms:field',
			formId: 12,
			fieldId: 'field-3',
		});
		const el = mountedElement();
		expect(el.props.formId).toBe(12);
		expect(el.props.fieldId).toBe('field-3');
	});

	it('routes nothing through BlockTextEditor or the image menu while a modal is active', () => {
		const { container } = renderWithSelected({
			blockType: 'core/site-title',
		});
		expect(container.firstChild).toBeNull();
		expect(
			document.getElementById('extendify-quick-edit-image-menu'),
		).toBeNull();
	});
});

describe('InlineEditor — modal onAfterSave reload contract', () => {
	// jsdom locks window.location.reload (configurable: false, writable: false),
	// same wrinkle the undo test calls out. Spy on console.error so the jsdom
	// "Not implemented: navigation" log doesn't trip @wordpress/jest-console,
	// then key off whether that log appeared to detect the reload call.
	let errorSpy;
	beforeEach(() => {
		errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
	});
	afterEach(() => {
		errorSpy.mockRestore();
	});

	const navigationLogged = () =>
		errorSpy.mock.calls.some((c) =>
			c.some((arg) => String(arg).includes('Not implemented: navigation')),
		);

	it('onAfterSave(true) closes the modal, clears selection, then attempts reload', () => {
		renderWithSelected({ blockType: 'core/site-title' });
		const el = mountedElement();
		el.props.onAfterSave(true);
		expect(mockCloseModal).toHaveBeenCalledWith(false);
		expect(mockClearSelected).toHaveBeenCalledTimes(1);
		expect(navigationLogged()).toBe(true);
	});

	it('onAfterSave(false) closes the modal + clears selection but skips reload', () => {
		renderWithSelected({ blockType: 'core/site-title' });
		const el = mountedElement();
		el.props.onAfterSave(false);
		expect(mockCloseModal).toHaveBeenCalledWith(false);
		expect(mockClearSelected).toHaveBeenCalledTimes(1);
		expect(navigationLogged()).toBe(false);
	});
});

describe('InlineEditor — modal unmount cleanup', () => {
	it('unmount closes any active modal (closeModal(false))', () => {
		const { unmount } = renderWithSelected({ blockType: 'core/site-title' });
		expect(mockCloseModal).not.toHaveBeenCalled();
		unmount();
		expect(mockCloseModal).toHaveBeenCalledWith(false);
	});
});

describe('InlineEditor — unsupported block type', () => {
	it('renders the UnsupportedNotice error pill with the block type', () => {
		renderWithSelected({
			blockType: 'core/something-weird',
			el: document.createElement('div'),
		});
		expect(document.body.textContent).toContain('core/something-weird');
		expect(mockMountModal).not.toHaveBeenCalled();
	});

	// The error pill announces assertively
	// and its × dismiss control carries an accessible name.
	it('error pill is role="alert" with an accessible Dismiss button', () => {
		renderWithSelected({
			blockType: 'core/something-weird',
			el: document.createElement('div'),
		});
		const pill = document.querySelector('[role="alert"]');
		expect(pill).not.toBeNull();
		expect(pill.querySelector('button').getAttribute('aria-label')).toBe(
			'Dismiss',
		);
	});
});