Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Add a flag (`useRenderToString`) to change the string renderer to use
`preact-render-to-string` to render components instead of rendering into the
DOM and reading the HTML output. This change enables using the string renderer
in non-DOM environments and more closely matches the React adapter's behavior.

- Add a feature flag (`simulateEventsOnComponents`) for supporting simulating
events on Components
[#211](https://github.com/preactjs/enzyme-adapter-preact-pure/pull/211)
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"mocha": "^10.0.0",
"nyc": "^15.1.0",
"preact": "^10.7.1",
"preact-render-to-string": "^5.2.6",
"prettier": "2.7.1",
"sinon": "^14.0.0",
"source-map-support": "^0.5.12",
Expand All @@ -35,7 +36,8 @@
},
"peerDependencies": {
"enzyme": "^3.11.0",
"preact": "^10.0.0"
"preact": "^10.0.0",
"preact-render-to-string": "^5.2.6"
},
"scripts": {
"build": "tsc --build tsconfig.json",
Expand Down
8 changes: 7 additions & 1 deletion src/Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface PreactAdapterOptions {
* behavior of the React 16 enzyme adapter.
*/
simulateEventsOnComponents?: boolean;

/**
* Flag to indicate to use preact-render-to-string for the 'string' enzyme
* renderer instead of mounting into a DOM and extracting the markup
*/
useRenderToString?: boolean;
}

export default class Adapter extends EnzymeAdapter {
Expand Down Expand Up @@ -68,7 +74,7 @@ export default class Adapter extends EnzymeAdapter {
case 'shallow':
return new ShallowRenderer({ ...this.preactAdapterOptions });
case 'string':
return new StringRenderer();
return new StringRenderer({ ...this.preactAdapterOptions });
default:
throw new Error(`"${options.mode}" rendering is not supported`);
}
Expand Down
18 changes: 13 additions & 5 deletions src/StringRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import type { Renderer, RSTNode } from 'enzyme';
import type { ReactElement } from 'react';
import { h, render } from 'preact';
import renderToString from 'preact-render-to-string';

import type { EventDetails } from './MountRenderer';
import type { PreactAdapterOptions } from './Adapter';

export default class StringRenderer implements Renderer {
constructor(private _options: PreactAdapterOptions) {}

render(el: ReactElement, context?: any) {
const tempContainer = document.createElement('div');
render(el as any, tempContainer);
const html = tempContainer.innerHTML;
render(h('unmount-me', {}), tempContainer);
return html;
if (this._options.useRenderToString) {
return renderToString(el, context);
} else {
const tempContainer = document.createElement('div');
render(el as any, tempContainer);
const html = tempContainer.innerHTML;
render(h('unmount-me', {}), tempContainer);
return html;
}
}

simulateError(nodeHierarchy: RSTNode[], rootNode: RSTNode, error: any) {
Expand Down
13 changes: 1 addition & 12 deletions test/init.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
// @ts-expect-error - JSDOM types are missing
import { JSDOM } from 'jsdom';
import minimist from 'minimist';
import { setupJSDOM } from './jsdom.js';

/* eslint-disable @typescript-eslint/no-var-requires */

// Setup DOM globals required by Preact rendering.
function setupJSDOM() {
// Enable `requestAnimationFrame` which Preact uses for scheduling hooks.
const dom = new JSDOM('', { pretendToBeVisual: true });
const g = global as any;
g.Event = dom.window.Event;
g.Node = dom.window.Node;
g.window = dom.window;
g.document = dom.window.document;
g.requestAnimationFrame = dom.window.requestAnimationFrame;
}
setupJSDOM();

// Support specifying a custom Preact library on the command line using
Expand Down
184 changes: 113 additions & 71 deletions test/integration_test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { assert } from 'chai';
import sinon from 'sinon';

import Adapter from '../src/Adapter.js';
import { setupJSDOM, teardownJSDOM } from './jsdom.js';

type TestContextValue = { myTestString: string };

Expand Down Expand Up @@ -43,6 +44,8 @@ interface Wrapper extends CommonWrapper {
* Register tests for static and interactive rendering modes.
*/
function addStaticTests(render: (el: ReactElement) => Wrapper) {
const isStringRenderer = (render as any) === renderToString;

it('renders a simple component', () => {
function Button({ label }: any) {
return <button>{label}</button>;
Expand All @@ -69,7 +72,7 @@ function addStaticTests(render: (el: ReactElement) => Wrapper) {
assert.equal(wrapper.html(), '<button>Click me</button>');
});

if ((render as any) !== renderToString) {
if (!isStringRenderer) {
it('can find DOM nodes by class name', () => {
function Widget() {
return <div class="widget">Test</div>;
Expand Down Expand Up @@ -112,7 +115,7 @@ function addStaticTests(render: (el: ReactElement) => Wrapper) {
});
}

if ((render as any) !== renderToString) {
if (!isStringRenderer) {
it('returns contents of fragments', () => {
const el = (
<div>
Expand All @@ -135,6 +138,9 @@ function addStaticTests(render: (el: ReactElement) => Wrapper) {
* Register tests for interactive rendering modes (full + shallow rendering).
*/
function addInteractiveTests(render: typeof mount) {
const isMount = (render as any) === mount;
const isShallow = (render as any) === shallow;

it('supports finding child components', () => {
function ListItem() {
return <li>Test</li>;
Expand Down Expand Up @@ -185,9 +191,9 @@ function addInteractiveTests(render: typeof mount) {

// nb. The node with `undefined` type is the Text node itself.
let expected: Array<string | preact.AnyComponent | undefined>;
if (render === mount) {
if (isMount) {
expected = [Widget, 'div', 'span', undefined];
} else if ((render as any) === shallow) {
} else if (isShallow) {
// Shallow rendering omits the top-level component in the output.
expected = ['div', 'span', undefined];
} else {
Expand Down Expand Up @@ -371,8 +377,7 @@ function addInteractiveTests(render: typeof mount) {
}

const wrapper = render(<Parent />);
const expectedText =
(render as any) === shallow ? '<Child />' : 'Everything is working';
const expectedText = isShallow ? '<Child />' : 'Everything is working';

// Initial render, we should see the original content.
assert.equal(wrapper.text(), expectedText);
Expand Down Expand Up @@ -416,54 +421,26 @@ function addInteractiveTests(render: typeof mount) {
});
}

const createDefaultAdapter = () => new Adapter();
function setAdapter(createNewAdapter: () => Adapter) {
beforeEach(() => {
configure({ adapter: createNewAdapter() });
});

afterEach(() => {
configure({ adapter: createDefaultAdapter() });
});
}

describe('integration tests', () => {
before(() => {
configure({ adapter: new Adapter() });
configure({ adapter: createDefaultAdapter() });
});

describe('"mount" rendering', () => {
addStaticTests(mount);
addInteractiveTests(mount);

it('supports simulating events on deep Components and elements', () => {
function FancyButton({ onClick, children }: any) {
return (
<button type="button" onClick={onClick}>
{children}
</button>
);
}

function FancierButton({ onClick, children }: any) {
return <FancyButton onClick={onClick}>{children}</FancyButton>;
}

function App() {
const [count, setCount] = useState(0);

return (
<div>
<div id="count">Count: {count}</div>
<FancierButton onClick={() => setCount(count + 1)}>
Increment
</FancierButton>
</div>
);
}

const wrapper = mount(<App />, {
// @ts-ignore This works but types don't say so
adapter: new Adapter({ simulateEventsOnComponents: true }),
});
assert.equal(wrapper.find('#count').text(), 'Count: 0');

wrapper.find(FancyButton).simulate('click');
assert.equal(wrapper.find('#count').text(), 'Count: 1');

wrapper.find('button').simulate('click');
assert.equal(wrapper.find('#count').text(), 'Count: 2');
});

it('supports retrieving elements', () => {
// Test workaround for bug where `Adapter.nodeToElement` is called
// with undefined `this` by `ReactWrapper#get`.
Expand Down Expand Up @@ -539,6 +516,46 @@ describe('integration tests', () => {
'<Provider value={{...}}><Component><span>override</span></Component></Provider>'
);
});

describe('simulateEventsOnComponents: true', () => {
setAdapter(() => new Adapter({ simulateEventsOnComponents: true }));

it('supports simulating events on deep Components and elements', () => {
function FancyButton({ onClick, children }: any) {
return (
<button type="button" onClick={onClick}>
{children}
</button>
);
}

function FancierButton({ onClick, children }: any) {
return <FancyButton onClick={onClick}>{children}</FancyButton>;
}

function App() {
const [count, setCount] = useState(0);

return (
<div>
<div id="count">Count: {count}</div>
<FancierButton onClick={() => setCount(count + 1)}>
Increment
</FancierButton>
</div>
);
}

const wrapper = mount(<App />);
assert.equal(wrapper.find('#count').text(), 'Count: 0');

wrapper.find(FancyButton).simulate('click');
assert.equal(wrapper.find('#count').text(), 'Count: 1');

wrapper.find('button').simulate('click');
assert.equal(wrapper.find('#count').text(), 'Count: 2');
});
});
});

describe('"shallow" rendering', () => {
Expand Down Expand Up @@ -761,39 +778,64 @@ describe('integration tests', () => {
assert.equal(wrapper.text(), 'Example');
});

it('supports simulating events on Components (simulateEventsOnComponents: true)', () => {
function FancyButton({ onClick, children }: any) {
return (
<button type="button" onClick={onClick}>
{children}
</button>
);
}
describe('simulateEventsOnComponents: true', () => {
setAdapter(() => new Adapter({ simulateEventsOnComponents: true }));

function App() {
const [count, setCount] = useState(0);
it('supports simulating events on Components', () => {
function FancyButton({ onClick, children }: any) {
return (
<button type="button" onClick={onClick}>
{children}
</button>
);
}

return (
<div>
<div id="count">Count: {count}</div>
<FancyButton onClick={() => setCount(count + 1)}>
Increment
</FancyButton>
</div>
);
}
function App() {
const [count, setCount] = useState(0);

return (
<div>
<div id="count">Count: {count}</div>
<FancyButton onClick={() => setCount(count + 1)}>
Increment
</FancyButton>
</div>
);
}

const wrapper = shallow(<App />, {
adapter: new Adapter({ simulateEventsOnComponents: true }),
});
assert.equal(wrapper.find('#count').text(), 'Count: 0');
const wrapper = shallow(<App />);
assert.equal(wrapper.find('#count').text(), 'Count: 0');

wrapper.find(FancyButton).simulate('click');
assert.equal(wrapper.find('#count').text(), 'Count: 1');
wrapper.find(FancyButton).simulate('click');
assert.equal(wrapper.find('#count').text(), 'Count: 1');
});
});
});

describe('"string" rendering', () => {
addStaticTests(renderToString as any);

describe('useRenderToString: true', () => {
setAdapter(() => new Adapter({ useRenderToString: true }));

// Ensure this flag works without a JSDOM environment so tear it down if
// it exists before running these tests
let reinitJSDOM = false;
before(() => {
if (global.window) {
reinitJSDOM = true;
teardownJSDOM();
}
});

after(() => {
if (reinitJSDOM) {
setupJSDOM();
reinitJSDOM = false;
}
});

addStaticTests(renderToString as any);
});
});
});
Loading