/**
* @jest-environment jsdom
* @jest-environment-options {"customExportConditions": ["node", "require", "default"]}
*
* Regression coverage for imperative re-render idempotency.
*
* Components that build their subtree imperatively in `@Are.onAfterMount`
* (the "dynamic component" pattern — the concrete child tag is only known
* from a prop at mount time) call `node.setContent(markup)` then
* `node.render()`. `@Are.onAfterMount` re-runs whenever the node is
* re-mounted — most importantly when an `AreRoot` outlet STASHES a subtree
* (unmount, keep in `AreRootCache`) and later RESTORES it (`child.mount()`)
* on tab switch.
*
* `render()` tokenizes `node.content` into child nodes; the tokenizer
* *appends* children. Without clearing first, a second `render()` (the
* re-mount) produced a DUPLICATE subtree, and the stale, already-live child
* was re-init/re-loaded — cascading scope-inheritance errors and a UI freeze.
*
* This test asserts `render()` is idempotent: re-rendering the same content
* rebuilds exactly one subtree, never accumulating duplicates.
*/
import { A_Concept, A_Context, A_Inject, A_Caller } from '@adaas/a-concept';
import { A_Config, ConfigReader } from '@adaas/a-utils/a-config';
import { A_Logger, A_LOGGER_ENV_KEYS } from '@adaas/a-utils/a-logger';
import { A_SignalBus, A_SignalState } from '@adaas/a-utils/a-signal';
import { A_Polyfill } from '@adaas/a-utils/a-polyfill';
import { Are, AreNode, AreStore, AreContainer, AreInit, AreRoute } from '@adaas/are';
import { AreRoot } from '@adaas/are-html/root/AreRoot.component';
import { AreHTMLEngine } from '@adaas/are-html/engine';
import { AreHTMLEngineContext } from '@adaas/are-html/context';
import { AreDirectiveFor } from '@adaas/are-html/directives/AreDirectiveFor.directive';
/** Leaf rendered by the imperative host. */
class RiLeaf extends Are {
@Are.Template
template(@A_Inject(A_Caller) node: AreNode): void {
node.setContent(`leaf`);
}
@Are.Data
data(@A_Inject(AreStore) store: AreStore): void { store.set({}); }
}
/**
* Imperative host — mirrors the AisChatDynamicComponent / AisChatHtmlComponent
* pattern: sets content + builds its subtree in onAfterMount via node.render().
* Captures the live node so the test can trigger a second render (a re-mount).
*/
class RiHost extends Are {
static lastNode: AreNode | undefined;
@Are.Template
template(@A_Inject(A_Caller) node: AreNode): void { node.setContent(''); }
@Are.Data
data(@A_Inject(AreStore) store: AreStore): void { store.set({}); }
@Are.onAfterMount
async onMount(@A_Inject(A_Caller) node: AreNode): Promise {
RiHost.lastNode = node;
node.setContent(``);
await node.render();
}
}
class RiApp extends Are {
@Are.Template
template(@A_Inject(A_Caller) node: AreNode): void {
node.setContent(`
`);
}
@Are.Data
data(@A_Inject(AreStore) store: AreStore): void { store.set({}); }
}
const flush = () => new Promise((r) => setTimeout(r, 0));
async function bootstrap(): Promise {
document.body.innerHTML = ``;
const container = new AreContainer({
name: 'ARE Render-Idempotency Test',
components: [
RiApp, RiHost, RiLeaf,
AreDirectiveFor, A_SignalBus, AreRoot, ConfigReader, AreHTMLEngine, A_Logger,
],
entities: [AreInit, AreRoute],
fragments: [
new A_SignalState([AreRoute]),
new AreHTMLEngineContext({ container: document }),
new A_Config({ defaults: { [A_LOGGER_ENV_KEYS.LOG_LEVEL]: 'error' } }),
],
});
const concept = new A_Concept({
name: 'adaas-are-html-render-idempotency-test',
fragments: [
new A_Config({
variables: ['CONFIG_VERBOSE', 'DEV_MODE'] as const,
defaults: { CONFIG_VERBOSE: false, DEV_MODE: false },
}),
],
components: [A_Logger, ConfigReader, A_Polyfill],
containers: [container],
});
await concept.load();
await concept.start();
await flush();
return concept;
}
afterEach(() => {
RiHost.lastNode = undefined;
A_Context.reset();
document.body.innerHTML = '';
});
describe('AreNode.render() idempotency', () => {
it('renders exactly one subtree on first mount', async () => {
await bootstrap();
expect(document.querySelectorAll('.ri-app .ri-leaf').length).toBe(1);
});
it('does NOT duplicate the subtree when render() runs again (re-mount)', async () => {
await bootstrap();
expect(document.querySelectorAll('.ri-app .ri-leaf').length).toBe(1);
// Simulate the AreRoot stash/restore re-mount: onAfterMount fires again,
// re-running setContent(sameMarkup) + render() on the SAME live node.
const node = RiHost.lastNode!;
expect(node).toBeDefined();
node.setContent(``);
await node.render();
await flush();
// After the fix render() clears the previous subtree first, so the DOM
// still has exactly one leaf — not two.
expect(document.querySelectorAll('.ri-app .ri-leaf').length).toBe(1);
});
});