Skip to content

Commit

Permalink
Merge branch 'master' into csb-ci
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock authored Nov 14, 2020
2 parents 618d870 + f64f811 commit 7355291
Show file tree
Hide file tree
Showing 31 changed files with 796 additions and 394 deletions.
7 changes: 2 additions & 5 deletions compat/src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ export interface Component<P = {}, S = {}> extends PreactComponent<P, S> {
isPureReactComponent?: true;
_patchedLifecycles?: true;

_childDidSuspend?(
error: Promise<void>,
suspendingComponent: Component<any, any>,
oldVNode?: VNode
): void;
_childDidSuspend?(error: Promise<void>, suspendingVNode: VNode): void;
_suspended: (vnode: VNode) => (unsuspend: () => void) => void;
_suspendedComponentWillUnmount?(): void;
}

Expand Down
9 changes: 6 additions & 3 deletions compat/src/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ const CAMEL_PROPS = /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|colo
// Input types for which onchange should not be converted to oninput.
// type="file|checkbox|radio", plus "range" in IE11.
// (IE11 doesn't support Symbol, which we use here to turn `rad` into `ra` which matches "range")
const ONCHANGE_INPUT_TYPES =
typeof Symbol != 'undefined' ? /fil|che|rad/i : /fil|che|ra/i;
const onChangeInputType = type =>
(typeof Symbol != 'undefined' && typeof Symbol() == 'symbol'
? /fil|che|rad/i
: /fil|che|ra/i
).test(type);

// Some libraries like `react-virtualized` explicitly check for this.
Component.prototype.isReactComponent = {};
Expand Down Expand Up @@ -129,7 +132,7 @@ options.vnode = vnode => {
i = 'ondblclick';
} else if (
/^onchange(textarea|input)/i.test(i + type) &&
!ONCHANGE_INPUT_TYPES.test(props.type)
!onChangeInputType(props.type)
) {
i = 'oninput';
} else if (/^on(Ani|Tra|Tou|BeforeInp)/.test(i)) {
Expand Down
15 changes: 9 additions & 6 deletions compat/src/suspense.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ options._catchError = function(error, newVNode, oldVNode) {
newVNode._children = oldVNode._children;
}
// Don't call oldCatchError if we found a Suspense
return component._childDidSuspend(error, newVNode._component);
return component._childDidSuspend(error, newVNode);
}
}
}
Expand Down Expand Up @@ -63,9 +63,11 @@ Suspense.prototype = new Component();

/**
* @param {Promise} promise The thrown promise
* @param {Component<any, any>} suspendingComponent The suspending component
* @param {import('./internal').VNode<any, any>} suspendingVNode The suspending component
*/
Suspense.prototype._childDidSuspend = function(promise, suspendingComponent) {
Suspense.prototype._childDidSuspend = function(promise, suspendingVNode) {
const suspendingComponent = suspendingVNode._component;

/** @type {import('./internal').SuspenseComponent} */
const c = this;

Expand Down Expand Up @@ -118,8 +120,7 @@ Suspense.prototype._childDidSuspend = function(promise, suspendingComponent) {
* to remain on screen and hydrate it when the suspense actually gets resolved.
* While in non-hydration cases the usual fallback -> component flow would occour.
*/
const vnode = c._vnode;
const wasHydrating = vnode && vnode._hydrating === true;
const wasHydrating = suspendingVNode._hydrating === true;
if (!wasHydrating && !c._pendingSuspensionCount++) {
c.setState({ _suspended: (c._detachOnNextRender = c._vnode._children[0]) });
}
Expand All @@ -141,6 +142,7 @@ Suspense.prototype.render = function(props, state) {
}

// Wrap fallback tree in a VNode that prevents itself from being marked as aborting mid-hydration:
/** @type {import('./internal').VNode} */
const fallback =
state._suspended && createElement(Fragment, null, props.fallback);
if (fallback) fallback._hydrating = null;
Expand All @@ -165,10 +167,11 @@ Suspense.prototype.render = function(props, state) {
* If the parent does not return a callback then the resolved vnode
* gets unsuspended immediately when it resolves.
*
* @param {import('../src/internal').VNode} vnode
* @param {import('./internal').VNode} vnode
* @returns {((unsuspend: () => void) => void)?}
*/
export function suspended(vnode) {
/** @type {import('./internal').Component} */
let component = vnode._parent._component;
return component && component._suspended && component._suspended(vnode);
}
Expand Down
9 changes: 8 additions & 1 deletion compat/test/browser/cloneElement.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ describe('compat cloneElement', () => {
a<span>b</span>
</foo>
);
expect(cloneElement(element)).to.eql(element);
const clone = cloneElement(element);
delete clone._original;
delete element._original;
expect(clone).to.eql(element);
});

it('should support props.children', () => {
let element = <foo children={<span>b</span>} />;
let clone = cloneElement(element);
delete clone._original;
delete element._original;
expect(clone).to.eql(element);
expect(cloneElement(clone).props.children).to.eql(element.props.children);
});
Expand All @@ -37,6 +42,8 @@ describe('compat cloneElement', () => {
</foo>
);
let clone = cloneElement(element);
delete clone._original;
delete element._original;
expect(clone).to.eql(element);
expect(clone.props.children.type).to.eql('div');
});
Expand Down
29 changes: 29 additions & 0 deletions compat/test/browser/events.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe('preact/compat events', () => {
const eventType = /Trident\//.test(navigator.userAgent)
? 'change'
: 'input';

render(<input type="range" onChange={() => null} />, scratch);
expect(proto.addEventListener).to.have.been.calledOnce;
expect(proto.addEventListener).to.have.been.calledWithExactly(
Expand All @@ -84,6 +85,34 @@ describe('preact/compat events', () => {
);
});

it('should normalize onChange for range, except in IE11, including when IE11 has Symbol polyfill', () => {
// NOTE: we don't normalize `onchange` for range inputs in IE11.
// This test mimics a specific scenario when a Symbol polyfill may
// be present, in which case onChange should still not be normalized

const isIE11 = /Trident\//.test(navigator.userAgent);
const eventType = isIE11 ? 'change' : 'input';

if (isIE11) {
window.Symbol = () => 'mockSymbolPolyfill';
}
sinon.spy(window, 'Symbol');

render(<input type="range" onChange={() => null} />, scratch);
expect(window.Symbol).to.have.been.calledOnce;
expect(proto.addEventListener).to.have.been.calledOnce;
expect(proto.addEventListener).to.have.been.calledWithExactly(
eventType,
sinon.match.func,
false
);

window.Symbol.restore();
if (isIE11) {
window.Symbol = undefined;
}
});

it('should support onAnimationEnd', () => {
const func = sinon.spy(() => {});
render(<div onAnimationEnd={func} />, scratch);
Expand Down
175 changes: 175 additions & 0 deletions compat/test/browser/suspense-hydration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { setupRerender } from 'preact/test-utils';
import React, {
createElement,
hydrate,
Component,
Fragment,
Suspense
} from 'preact/compat';
import { logCall, getLog, clearLog } from '../../../test/_util/logCall';
import { setupScratch, teardown } from '../../../test/_util/helpers';
import { createLazy } from './suspense-utils';

/* eslint-env browser, mocha */
describe('suspense hydration', () => {
/** @type {HTMLDivElement} */
let scratch,
rerender,
unhandledEvents = [];

function onUnhandledRejection(event) {
unhandledEvents.push(event);
}

/** @type {() => void} */
let increment;
class Counter extends Component {
constructor(props) {
super(props);

this.state = { count: 0 };
increment = () => this.setState({ count: ++this.state.count });
}

render(props, { count }) {
return <div>Count: {count}</div>;
}
}

let resetAppendChild;
let resetInsertBefore;
let resetRemoveChild;
let resetRemove;

before(() => {
resetAppendChild = logCall(Element.prototype, 'appendChild');
resetInsertBefore = logCall(Element.prototype, 'insertBefore');
resetRemoveChild = logCall(Element.prototype, 'removeChild');
resetRemove = logCall(Element.prototype, 'remove');
});

after(() => {
resetAppendChild();
resetInsertBefore();
resetRemoveChild();
resetRemove();
});

beforeEach(() => {
scratch = setupScratch();
rerender = setupRerender();

unhandledEvents = [];
if ('onunhandledrejection' in window) {
window.addEventListener('unhandledrejection', onUnhandledRejection);
}
});

afterEach(() => {
teardown(scratch);

if ('onunhandledrejection' in window) {
window.removeEventListener('unhandledrejection', onUnhandledRejection);

if (unhandledEvents.length) {
throw unhandledEvents[0].reason;
}
}
});

it('should leave DOM untouched when suspending while hydrating', () => {
scratch.innerHTML = '<div>Hello</div>';
clearLog();

const [Lazy, resolve] = createLazy();
hydrate(
<Suspense>
<Lazy />
</Suspense>,
scratch
);
rerender(); // Flush rerender queue to mimic what preact will really do
expect(scratch.innerHTML).to.equal('<div>Hello</div>');
expect(getLog()).to.deep.equal([]);
clearLog();

return resolve(() => <div>Hello</div>).then(() => {
rerender();
expect(scratch.innerHTML).to.equal('<div>Hello</div>');
expect(getLog()).to.deep.equal([]);
clearLog();
});
});

it('should allow siblings to update around suspense boundary', () => {
scratch.innerHTML = '<div>Count: 0</div><div>Hello</div>';
clearLog();

const [Lazy, resolve] = createLazy();
hydrate(
<Fragment>
<Counter />
<Suspense>
<Lazy />
</Suspense>
</Fragment>,
scratch
);
rerender(); // Flush rerender queue to mimic what preact will really do
expect(scratch.innerHTML).to.equal('<div>Count: 0</div><div>Hello</div>');
// Re: DOM OP below - Known issue with hydrating merged text nodes
expect(getLog()).to.deep.equal(['<div>Count: .appendChild(#text)']);
clearLog();

increment();
rerender();

expect(scratch.innerHTML).to.equal('<div>Count: 1</div><div>Hello</div>');
expect(getLog()).to.deep.equal([]);
clearLog();

return resolve(() => <div>Hello</div>).then(() => {
rerender();
expect(scratch.innerHTML).to.equal('<div>Count: 1</div><div>Hello</div>');
expect(getLog()).to.deep.equal([]);
clearLog();
});
});

it('should properly hydrate when there is DOM and Components between Suspense and suspender', () => {
scratch.innerHTML = '<div><div>Hello</div></div>';
clearLog();

const [Lazy, resolve] = createLazy();
hydrate(
<Suspense>
<div>
<Fragment>
<Lazy />
</Fragment>
</div>
</Suspense>,
scratch
);
rerender(); // Flush rerender queue to mimic what preact will really do
expect(scratch.innerHTML).to.equal('<div><div>Hello</div></div>');
expect(getLog()).to.deep.equal([]);
clearLog();

return resolve(() => <div>Hello</div>).then(() => {
rerender();
expect(scratch.innerHTML).to.equal('<div><div>Hello</div></div>');
expect(getLog()).to.deep.equal([]);
clearLog();
});
});

// TODO:
// 1. What if props change between when hydrate suspended and suspense
// resolves?
// 2. If using real Suspense, test re-suspending after hydrate suspense
// 3. Put some DOM and components with state and event listeners between
// suspender and Suspense boundary
// 4. Put some sibling DOM and components with state and event listeners
// sibling to suspender and under Suspense boundary
});
Loading

0 comments on commit 7355291

Please sign in to comment.