HEX
Server: LiteSpeed
System: Linux server107.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: iddeczhh (1154)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/iddeczhh/public_html/wp-content/plugins/extendify/tests/unit/Toolbar/toolbar.test.js
/**
 * Characterization tests for the simple Extendify toolbar bridge.
 *
 * The toolbar.js module is a vanilla-JS bootstrap with no exports —
 * it self-runs `init()` at import time. Each test rebuilds the DOM
 * and uses `jest.isolateModulesAsync` so the side effects of one
 * test never leak into the next.
 *
 * Pinned behaviors:
 *  - No-op when `#extendify-toolbar` is missing (theme without
 *    `wp_body_open` won't blow up).
 *  - AI Agent button starts disabled with a loading title, then
 *    enables once the Agent has mounted its admin-bar button.
 *  - AI Agent click drives the Agent's mounted host button.
 *  - Quick Edit click dispatches the `extendify-quick-edit:toggle`
 *    custom event and prevents default navigation.
 *  - `aria-checked` mirrors the `extendify-quick-edit-on` class
 *    on `<html>` so any toggle source keeps the radio visually
 *    in sync.
 *  - When the Agent sidebar opens (its `inert` attribute is
 *    removed) the toolbar gets `ext-tb-agent-open`. When it
 *    closes (the `inert` attribute is set), the class is removed.
 *
 * Characterization finding — sidebar watcher is timing-sensitive:
 * `watchAgentSidebar` only attaches the inner `inert` observer when
 * its body-level MutationObserver fires. The `takeRecords()` fallback
 * for an already-mounted sidebar empties the queue but does NOT
 * invoke the callback, so a sidebar that exists BEFORE toolbar.js
 * initializes is silently ignored — its `inert` changes never flip
 * `ext-tb-agent-open`. In production this is invisible: toolbar.js
 * runs at DOMContentLoaded and the Agent mounts its sidebar later
 * via React, so the late-mount path is the one that actually fires.
 */

const renderToolbar = () => {
	document.body.innerHTML = `
		<div id="extendify-toolbar">
			<button type="button" id="ext-tb-ai-agent">AI Agent</button>
			<button type="button" id="ext-tb-quick-edit" role="switch" aria-checked="false">Quick Edit</button>
		</div>
	`;
};

const mountAgentButton = () => {
	const host = document.createElement('li');
	host.id = 'wp-admin-bar-extendify-agent-btn';
	const inner = document.createElement('button');
	inner.type = 'button';
	host.appendChild(inner);
	document.body.appendChild(host);
	return inner;
};

const mountAgentSidebar = ({ open }) => {
	const node = document.createElement('div');
	node.id = 'extendify-agent-sidebar';
	if (!open) node.setAttribute('inert', '');
	document.body.appendChild(node);
	return node;
};

const tick = () => new Promise((r) => setTimeout(r, 0));

const importToolbar = async () => {
	await jest.isolateModulesAsync(async () => {
		await import('@toolbar/toolbar');
	});
};

beforeEach(() => {
	jest.resetModules();
	jest.useRealTimers();
	document.documentElement.className = '';
	document.body.innerHTML = '';
});

describe('toolbar — no-op when host missing', () => {
	it('returns silently when #extendify-toolbar is not in the DOM', async () => {
		await expect(importToolbar()).resolves.toBeUndefined();
	});
});

describe('toolbar — AI Agent button loading state', () => {
	it('starts disabled with a loading title and aria-busy=true', async () => {
		jest.useFakeTimers();
		renderToolbar();

		await importToolbar();

		const btn = document.getElementById('ext-tb-ai-agent');
		expect(btn.disabled).toBe(true);
		expect(btn.title).toBe('Loading…');
		expect(btn.getAttribute('aria-busy')).toBe('true');
	});

	it('enables and clears the loading title once the Agent button mounts', async () => {
		jest.useFakeTimers();
		renderToolbar();

		await importToolbar();

		mountAgentButton();
		await jest.advanceTimersByTimeAsync(50);

		const btn = document.getElementById('ext-tb-ai-agent');
		expect(btn.disabled).toBe(false);
		expect(btn.title).toBe('');
		expect(btn.getAttribute('aria-busy')).toBe('false');
	});

	it('falls back to an unavailable title when the Agent never appears', async () => {
		jest.useFakeTimers();
		renderToolbar();

		await importToolbar();

		// Module polls 100 times at 50ms intervals. Drain them all.
		await jest.advanceTimersByTimeAsync(100 * 50 + 10);

		const btn = document.getElementById('ext-tb-ai-agent');
		expect(btn.title).toBe('AI Agent unavailable');
		expect(btn.getAttribute('aria-busy')).toBe('false');
	});
});

describe('toolbar — AI Agent click drives the Agent host button', () => {
	it('clicks the mounted Agent button when it is already present', async () => {
		renderToolbar();
		const agentBtn = mountAgentButton();
		const clickSpy = jest.spyOn(agentBtn, 'click');

		await importToolbar();
		await tick();

		const aiBtn = document.getElementById('ext-tb-ai-agent');
		const event = new MouseEvent('click', { bubbles: true, cancelable: true });
		const preventSpy = jest.spyOn(event, 'preventDefault');
		aiBtn.dispatchEvent(event);

		expect(preventSpy).toHaveBeenCalled();
		expect(clickSpy).toHaveBeenCalledTimes(1);
	});

	it('clicks the host element itself when it has no inner button', async () => {
		renderToolbar();
		const host = document.createElement('li');
		host.id = 'wp-admin-bar-extendify-agent-btn';
		host.click = jest.fn();
		document.body.appendChild(host);

		await importToolbar();
		await tick();

		document
			.getElementById('ext-tb-ai-agent')
			.dispatchEvent(
				new MouseEvent('click', { bubbles: true, cancelable: true }),
			);

		expect(host.click).toHaveBeenCalled();
	});
});

describe('toolbar — Quick Edit toggle event', () => {
	it('dispatches extendify-quick-edit:toggle and prevents default on click', async () => {
		renderToolbar();
		await importToolbar();

		const listener = jest.fn();
		window.addEventListener('extendify-quick-edit:toggle', listener);

		const btn = document.getElementById('ext-tb-quick-edit');
		const event = new MouseEvent('click', { bubbles: true, cancelable: true });
		const preventSpy = jest.spyOn(event, 'preventDefault');
		btn.dispatchEvent(event);

		expect(preventSpy).toHaveBeenCalled();
		expect(listener).toHaveBeenCalledTimes(1);

		window.removeEventListener('extendify-quick-edit:toggle', listener);
	});
});

describe('toolbar — aria-checked mirrors the html.extendify-quick-edit-on class', () => {
	it('initial sync writes aria-checked=false when the class is absent', async () => {
		renderToolbar();
		await importToolbar();

		expect(
			document.getElementById('ext-tb-quick-edit').getAttribute('aria-checked'),
		).toBe('false');
	});

	it('initial sync writes aria-checked=true when the class is already present', async () => {
		document.documentElement.classList.add('extendify-quick-edit-on');
		renderToolbar();

		await importToolbar();

		expect(
			document.getElementById('ext-tb-quick-edit').getAttribute('aria-checked'),
		).toBe('true');
	});

	it('flips aria-checked to true when the class is added later', async () => {
		renderToolbar();
		await importToolbar();

		document.documentElement.classList.add('extendify-quick-edit-on');
		await tick();

		expect(
			document.getElementById('ext-tb-quick-edit').getAttribute('aria-checked'),
		).toBe('true');
	});

	it('flips aria-checked back to false when the class is removed', async () => {
		document.documentElement.classList.add('extendify-quick-edit-on');
		renderToolbar();
		await importToolbar();

		document.documentElement.classList.remove('extendify-quick-edit-on');
		await tick();

		expect(
			document.getElementById('ext-tb-quick-edit').getAttribute('aria-checked'),
		).toBe('false');
	});
});

describe('toolbar — Agent sidebar inert watcher', () => {
	it('picks up a sidebar that mounts AFTER the toolbar has loaded (open)', async () => {
		renderToolbar();
		await importToolbar();

		mountAgentSidebar({ open: true });
		await tick();

		const toolbar = document.getElementById('extendify-toolbar');
		expect(toolbar.classList.contains('ext-tb-agent-open')).toBe(true);
	});

	it('picks up a sidebar that mounts AFTER the toolbar has loaded (closed)', async () => {
		renderToolbar();
		await importToolbar();

		mountAgentSidebar({ open: false });
		await tick();

		const toolbar = document.getElementById('extendify-toolbar');
		expect(toolbar.classList.contains('ext-tb-agent-open')).toBe(false);
	});

	it('toggles ext-tb-agent-open when the sidebar inert attribute changes', async () => {
		renderToolbar();
		await importToolbar();

		const sidebar = mountAgentSidebar({ open: false });
		await tick();

		const toolbar = document.getElementById('extendify-toolbar');
		expect(toolbar.classList.contains('ext-tb-agent-open')).toBe(false);

		sidebar.removeAttribute('inert');
		await tick();
		expect(toolbar.classList.contains('ext-tb-agent-open')).toBe(true);

		sidebar.setAttribute('inert', '');
		await tick();
		expect(toolbar.classList.contains('ext-tb-agent-open')).toBe(false);
	});

	it('silently ignores a sidebar that was already in the DOM before init (known timing gap)', async () => {
		// `watchAgentSidebar` calls `observer.takeRecords()` for the
		// pre-existing case, but that empties the queue without firing
		// the callback — so the inner inert observer never attaches.
		// Class stays unset and later inert changes are not picked up.
		renderToolbar();
		const sidebar = mountAgentSidebar({ open: true });

		await importToolbar();
		await tick();

		const toolbar = document.getElementById('extendify-toolbar');
		expect(toolbar.classList.contains('ext-tb-agent-open')).toBe(false);

		sidebar.setAttribute('inert', '');
		await tick();
		sidebar.removeAttribute('inert');
		await tick();

		expect(toolbar.classList.contains('ext-tb-agent-open')).toBe(false);
	});
});