Skip to content

Commit

Permalink
fix(testing): jest component disconnected callback (#4269)
Browse files Browse the repository at this point in the history
* add method to remove DOM nodes when tearing down test

This commit adds a method to the Jest environment setup file that will handle removing each node from the mocked DOM. If we do not explicitly remove each node, then the `disconnectedCallback` will not execute for the Stencil component(s). In a testing context, this can be problematic if this lifecycle method contains cleanup code (for things like timeouts and other async functionality) that should execute after the test has ran.

* only remove children of `body`

* test `removeDomNodes()`

* fix test cases
  • Loading branch information
tanner-reits committed Apr 26, 2023
1 parent 8ca9058 commit 4ec3b69
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 1 deletion.
38 changes: 37 additions & 1 deletion src/testing/jest/jest-setup-test-framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
setErrorHandler,
stopAutoApplyChanges,
} from '@stencil/core/internal/testing';
import { setupGlobal, teardownGlobal } from '@stencil/core/mock-doc';
import { MockDocument, MockNode, MockWindow, setupGlobal, teardownGlobal } from '@stencil/core/mock-doc';

import { expectExtend } from '../matchers';
import { setupMockFetch } from '../mock-fetch';
Expand Down Expand Up @@ -40,6 +40,24 @@ export function jestSetupTestFramework() {
}
stopAutoApplyChanges();

// Remove each node from the mocked DOM
// Without this step, a component's `disconnectedCallback`
// will not be called since this only happens when a node is removed,
// not if the window is destroyed.
//
// So, we do this outside the mocked window/DOM teardown
// because this operation is really only necessary in the testing
// context so any "cleanup" operations in the `disconnectedCallback`
// can happen to prevent testing errors with async code in the component
//
// We only care about removing all the nodes that are children of the 'body' tag/node.
// This node is a child of the `html` tag which is the 2nd child of the document (hence
// the `1` index).
const bodyNode = (
((global as any).window as MockWindow)?.document as unknown as MockDocument
)?.childNodes?.[1]?.childNodes?.find((ref) => ref.nodeName === 'BODY');
bodyNode?.childNodes?.forEach(removeDomNodes);

teardownGlobal(global);
global.Context = {};
global.resourcesUrl = '/build';
Expand Down Expand Up @@ -72,3 +90,21 @@ export function jestSetupTestFramework() {
Object.assign(Env, stencilEnv);
}
}

/**
* Recursively removes all child nodes of a passed node starting with the
* furthest descendant and then moving back up the DOM tree.
*
* @param node The mocked DOM node that will be removed from the DOM
*/
export function removeDomNodes(node: MockNode) {
if (node == null) {
return;
}

if (!node.childNodes?.length) {
node.remove();
}

node.childNodes?.forEach(removeDomNodes);
}
43 changes: 43 additions & 0 deletions src/testing/jest/test/jest-setup-test-framework.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { MockHTMLElement, MockNode } from '../../../mock-doc/node';
import { removeDomNodes } from '../jest-setup-test-framework';

describe('jest setup test framework', () => {
describe('removeDomNodes', () => {
it('removes all children of the parent node', () => {
const parentNode = new MockHTMLElement(null, 'div');
parentNode.appendChild(new MockHTMLElement(null, 'p'));

expect(parentNode.childNodes.length).toEqual(1);

removeDomNodes(parentNode);

expect(parentNode.childNodes.length).toBe(0);
});

it('does nothing if there is no parent node', () => {
const parentNode: MockNode = undefined;

removeDomNodes(parentNode);

expect(parentNode).toBeUndefined();
});

it('does nothing if the parent node child array is empty', () => {
const parentNode = new MockHTMLElement(null, 'div');
parentNode.childNodes = [];

removeDomNodes(parentNode);

expect(parentNode.childNodes).toStrictEqual([]);
});

it('does nothing if the parent node child array is `null`', () => {
const parentNode = new MockHTMLElement(null, 'div');
parentNode.childNodes = null;

removeDomNodes(parentNode);

expect(parentNode.childNodes).toBe(null);
});
});
});

0 comments on commit 4ec3b69

Please sign in to comment.