Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(testing): jest component disconnected callback #4269

Merged
merged 6 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/testing/jest/jest-setup-test-framework.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could add a test for this either in end-to-end or karma - thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted with @rwaskiewicz about this and came to the conclusion that there isn't a good, reliable way to test the teardown behavior since that logic executes in a global afterEach() callback. Hence, trying to spy on any component's disconnectedCallback or public class member won't work since a component's teardown wouldn't happen until after the test has finished and any assertions have already run.

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);
});
});
});