eb651180aa
Front components are third-party UI that runs in a sandboxed worker, so every DOM event reaching them has to be on an explicit allow-list. That list was small: mostly click, focus and pointer events. This adds touch, drag and drop, focusin/focusout, animationend/transitionend and scrollend, plus load/error on `<img>` and toggle on `<details>`/`<dialog>`. Two of them need the host to do more than forward the event: - react-dom has no `onFocusIn`/`onFocusOut` props, so the host attaches those two with `addEventListener` instead. - a browser only fires `drop` on an element whose `dragover` default was prevented, and the component's own `preventDefault` arrives too late across the async worker boundary. The host prevents it synchronously as soon as the component declares either handler. Touch events carry their coordinates on `changedTouches`, so the first touch fills the existing coordinate fields. Still not crossing, since each would need a new serialized field: touch lists, `animationName`/`propertyName`/`elapsedTime`, toggle `newState` and `dataTransfer`. The diff also renames a few things it touches (`filterProps` and `EventToReact` in particular) so the host-side event path reads in order. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22616?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
194 lines
5.6 KiB
TypeScript
194 lines
5.6 KiB
TypeScript
import { DOM_EVENT_TYPE_TO_REACT_PROP } from '@/constants/DomEventTypeToReactProp';
|
|
|
|
import { buildHostReactPropsFromRemoteProps } from '../buildHostReactPropsFromRemoteProps';
|
|
|
|
describe('buildHostReactPropsFromRemoteProps', () => {
|
|
it('should drop internal remote-dom props', () => {
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ element: {}, receiver: {}, components: {}, id: 'keep' },
|
|
'div',
|
|
);
|
|
|
|
expect(result).toEqual({ id: 'keep' });
|
|
});
|
|
|
|
it('should drop undefined values', () => {
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ title: undefined, id: 'x' },
|
|
'div',
|
|
);
|
|
|
|
expect('title' in result).toBe(false);
|
|
expect(result.id).toBe('x');
|
|
});
|
|
|
|
it('should parse the style string into an object', () => {
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ style: 'color: red' },
|
|
'div',
|
|
);
|
|
|
|
expect(result.style).toEqual({ color: 'red' });
|
|
});
|
|
|
|
it('should wrap function event handlers and normalize their key', () => {
|
|
const onClick = jest.fn();
|
|
const result = buildHostReactPropsFromRemoteProps({ onClick }, 'div');
|
|
|
|
expect(typeof result.onClick).toBe('function');
|
|
expect(result.onClick).not.toBe(onClick);
|
|
});
|
|
|
|
it('should normalize and wrap newly allowed event handlers', () => {
|
|
const handler = jest.fn();
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{
|
|
onTouchstart: handler,
|
|
onDragstart: handler,
|
|
onDrop: handler,
|
|
onAnimationend: handler,
|
|
onTransitionend: handler,
|
|
onScrollend: handler,
|
|
onToggle: handler,
|
|
onLoad: handler,
|
|
},
|
|
'div',
|
|
);
|
|
|
|
expect(typeof result.onTouchStart).toBe('function');
|
|
expect(typeof result.onDragStart).toBe('function');
|
|
expect(typeof result.onDrop).toBe('function');
|
|
expect(typeof result.onAnimationEnd).toBe('function');
|
|
expect(typeof result.onTransitionEnd).toBe('function');
|
|
expect(typeof result.onScrollEnd).toBe('function');
|
|
expect(typeof result.onToggle).toBe('function');
|
|
expect(typeof result.onLoad).toBe('function');
|
|
});
|
|
|
|
it('should normalize focusin and focusout handlers to their react-style keys', () => {
|
|
const handler = jest.fn();
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ onFocusin: handler, onFocusout: handler },
|
|
'div',
|
|
);
|
|
|
|
expect(typeof result.onFocusIn).toBe('function');
|
|
expect(typeof result.onFocusOut).toBe('function');
|
|
});
|
|
|
|
it('should drop event-handler props whose value is not a function', () => {
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ onClick: 'alert(1)', onmouseover: 'x' },
|
|
'div',
|
|
);
|
|
|
|
expect('onClick' in result).toBe(false);
|
|
expect('onMouseOver' in result).toBe(false);
|
|
expect('onmouseover' in result).toBe(false);
|
|
});
|
|
|
|
it('should drop handler props whose event type is not allow-listed', () => {
|
|
const handler = jest.fn();
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{
|
|
onCopy: handler,
|
|
onCut: handler,
|
|
onPaste: handler,
|
|
onSelect: handler,
|
|
onBeforeInput: handler,
|
|
},
|
|
'div',
|
|
);
|
|
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it('should keep every allow-listed event under its react prop name', () => {
|
|
const handler = jest.fn();
|
|
|
|
for (const reactProp of Object.values(DOM_EVENT_TYPE_TO_REACT_PROP)) {
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ [reactProp]: handler },
|
|
'div',
|
|
);
|
|
|
|
expect(Object.keys(result)).toEqual([reactProp]);
|
|
}
|
|
});
|
|
|
|
it('should drop capture-phase handler props', () => {
|
|
const handler = jest.fn();
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ onClickCapture: handler, onKeyDownCapture: handler },
|
|
'div',
|
|
);
|
|
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it('should drop a dangerous scheme on a navigation attribute', () => {
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ href: 'javascript:alert(1)' },
|
|
'a',
|
|
);
|
|
|
|
expect('href' in result).toBe(false);
|
|
});
|
|
|
|
it('should keep a dangerous scheme on a non-navigation attribute', () => {
|
|
const dataImage = 'data:image/png;base64,iVBOR';
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ src: dataImage },
|
|
'img',
|
|
);
|
|
|
|
expect(result.src).toBe(dataImage);
|
|
});
|
|
|
|
it('should keep a safe url on a navigation attribute', () => {
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ href: 'https://twenty.com' },
|
|
'a',
|
|
);
|
|
|
|
expect(result.href).toBe('https://twenty.com');
|
|
});
|
|
|
|
it('should forward arbitrary aria-* and data-* attributes', () => {
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{
|
|
'aria-selected': 'true',
|
|
'aria-activedescendant': 'item-2',
|
|
'data-state': 'open',
|
|
'data-count': '3',
|
|
},
|
|
'div',
|
|
);
|
|
|
|
expect(result['aria-selected']).toBe('true');
|
|
expect(result['aria-activedescendant']).toBe('item-2');
|
|
expect(result['data-state']).toBe('open');
|
|
expect(result['data-count']).toBe('3');
|
|
});
|
|
|
|
it('should forward the draggable attribute', () => {
|
|
expect(
|
|
buildHostReactPropsFromRemoteProps({ draggable: 'true' }, 'div')
|
|
.draggable,
|
|
).toBe('true');
|
|
expect(
|
|
buildHostReactPropsFromRemoteProps({ draggable: true }, 'div').draggable,
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should still drop a non-function on* handler smuggled as a data-adjacent prop', () => {
|
|
const result = buildHostReactPropsFromRemoteProps(
|
|
{ onClick: 'alert(1)', 'data-state': 'open' },
|
|
'div',
|
|
);
|
|
|
|
expect('onClick' in result).toBe(false);
|
|
expect(result['data-state']).toBe('open');
|
|
});
|
|
});
|