diff --git a/packages/designer/.eslintrc.js b/packages/designer/.eslintrc.js index 35f9add82..cf6223647 100644 --- a/packages/designer/.eslintrc.js +++ b/packages/designer/.eslintrc.js @@ -3,7 +3,7 @@ module.exports = { ignorePatterns: [ 'tests/* '], rules: { 'react/no-multi-comp': 0, - 'no-unused-expressions': 1, + 'no-unused-expressions': 0, 'implicit-arrow-linebreak': 1, 'no-nested-ternary': 1, 'no-mixed-operators': 1, diff --git a/packages/designer/jest.config.js b/packages/designer/jest.config.js index bfb49f2ac..c5f407a6a 100644 --- a/packages/designer/jest.config.js +++ b/packages/designer/jest.config.js @@ -11,6 +11,7 @@ module.exports = { transformIgnorePatterns: [ `/node_modules/(?!${esModules})/`, ], + setupFiles: ['./tests/fixtures/unhandled-rejection.ts'], moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], collectCoverage: false, collectCoverageFrom: [ diff --git a/packages/designer/package.json b/packages/designer/package.json index c392b9a55..21695337f 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@ali/lowcode-test-mate": "^1.0.1", - "@alib/build-scripts": "^0.1.18", + "@alib/build-scripts": "^0.1.29", "@types/classnames": "^2.2.7", "@types/medium-editor": "^5.0.3", "@types/node": "^13.7.1", diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts index 9b853422e..afea40812 100644 --- a/packages/designer/src/builtin-simulator/host.ts +++ b/packages/designer/src/builtin-simulator/host.ts @@ -473,12 +473,12 @@ export class BuiltinSimulatorHost implements ISimulatorHost { - detecting.leave(this.project.currentDocument); - doc.removeEventListener('mouseover', hover, true); - doc.removeEventListener('mouseleave', leave, false); - this.disableDetecting = undefined; - }; + // this.disableDetecting = () => { + // detecting.leave(this.project.currentDocument); + // doc.removeEventListener('mouseover', hover, true); + // doc.removeEventListener('mouseleave', leave, false); + // this.disableDetecting = undefined; + // }; } readonly liveEditing = new LiveEditing(); @@ -525,21 +525,22 @@ export class BuiltinSimulatorHost implements ISimulatorHost { +describe('host-view 测试', () => { let designer: Designer; beforeEach(() => { designer = new Designer({ editor }); @@ -26,9 +26,7 @@ describe('setting-prop-entry 测试', () => { designer = null; }); - it('xxx', () => { - // console.log(JSON.stringify(TestRenderer.create().toJSON())); - - console.log(render( { console.log('xxx', xxx)}}/>)) + it('host-view', () => { + const hostView = render(); }) }); diff --git a/packages/designer/tests/builtin-simulator/host.test.tsx b/packages/designer/tests/builtin-simulator/host.test.tsx index fcc3fdb84..fdeb0943a 100644 --- a/packages/designer/tests/builtin-simulator/host.test.tsx +++ b/packages/designer/tests/builtin-simulator/host.test.tsx @@ -3,17 +3,25 @@ import set from 'lodash/set'; import cloneDeep from 'lodash/clonedeep'; import '../fixtures/window'; import { Editor } from '@ali/lowcode-editor-core'; +import { + AssetLevel, + Asset, + AssetList, + assetBundle, + assetItem, + AssetType, +} from '@ali/lowcode-utils'; import { Project } from '../../src/project/project'; import { Node } from '../../src/document/node/node'; import { Designer } from '../../src/designer/designer'; import formSchema from '../fixtures/schema/form'; -import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; +import { getMockDocument, getMockWindow, getMockEvent } from '../utils'; import { BuiltinSimulatorHost } from '../../src/builtin-simulator/host'; - +import { eq } from 'lodash'; const editor = new Editor(); -describe('setting-prop-entry 测试', () => { +describe('host 测试', () => { let designer: Designer; beforeEach(() => { designer = new Designer({ editor }); @@ -23,7 +31,115 @@ describe('setting-prop-entry 测试', () => { designer = null; }); - it('dummy test', () => { - console.log(new BuiltinSimulatorHost(designer.project)); + it('基础方法测试', async () => { + const host = new BuiltinSimulatorHost(designer.project); + expect(host.currentDocument).toBe(designer.project.currentDocument); + expect(host.renderEnv).toBe('default'); + expect(host.device).toBe('default'); + expect(host.deviceClassName).toBeUndefined; + host.setProps({ + renderEnv: 'rax', + device: 'mobile', + deviceClassName: 'mobile-rocks', + componentsAsset: [{ + type: AssetType.JSText, + content: 'console.log(1)', + }, { + type: AssetType.JSUrl, + content: '//path/to/js', + }], + theme: { + type: AssetType.CSSText, + content: '.theme {font-size: 50px;}', + } + }); + expect(host.renderEnv).toBe('rax'); + expect(host.device).toBe('mobile'); + expect(host.deviceClassName).toBe('mobile-rocks'); + expect(host.componentsAsset).toEqual([{ + type: AssetType.JSText, + content: 'console.log(1)', + }, { + type: AssetType.JSUrl, + content: '//path/to/js', + }]); + expect(host.theme).toEqual({ + type: AssetType.CSSText, + content: '.theme {font-size: 50px;}', + }); + expect(host.componentsMap).toBe(designer.componentsMap); + + host.set('renderEnv', 'vue'); + expect(host.renderEnv).toBe('vue'); + + expect(host.getComponentContext).toThrow('Method not implemented.'); + }); + + it('事件测试', async () => { + const host = new BuiltinSimulatorHost(designer.project); + const mockDocument = getMockDocument(); + const mockWindow = getMockWindow(mockDocument); + const mockIframe = { + contentWindow: mockWindow, + contentDocument: mockDocument, + dispatchEvent() {}, + }; + + // 非法分支测试 + host.mountContentFrame(); + expect(host._iframe).toBeUndefined(); + + host.set('library', [{ + package: '@ali/vc-deep', + library: 'lib', + urls: ['a.js', 'b.js'] + }]); + + host.componentsConsumer.consume(() => {}); + host.injectionConsumer.consume(() => {}); + await host.mountContentFrame(mockIframe); + + expect(host.contentWindow).toBe(mockWindow); + + mockDocument.triggerEventListener( + 'mouseover', + getMockEvent(mockDocument.createElement('div')), + host, + ); + mockDocument.triggerEventListener( + 'mouseleave', + getMockEvent(mockDocument.createElement('div')), + host, + ); + mockDocument.triggerEventListener( + 'mousedown', + getMockEvent(mockDocument.createElement('div')), + host, + ); + mockDocument.triggerEventListener( + 'mouseup', + getMockEvent(mockDocument.createElement('div')), + host, + ); + mockDocument.triggerEventListener( + 'mousemove', + getMockEvent(mockDocument.createElement('div')), + host, + ); + mockDocument.triggerEventListener( + 'click', + getMockEvent(document.createElement('input')), + host, + ); + mockDocument.triggerEventListener( + 'dblclick', + getMockEvent(mockDocument.createElement('div')), + host, + ); + mockDocument.triggerEventListener( + 'contextmenu', + getMockEvent(mockDocument.createElement('div')), + host, + ); }) }); diff --git a/packages/designer/tests/builtin-simulator/parse-metadata.test.ts b/packages/designer/tests/builtin-simulator/parse-metadata.test.ts index eefb55f7c..50a1ba005 100644 --- a/packages/designer/tests/builtin-simulator/parse-metadata.test.ts +++ b/packages/designer/tests/builtin-simulator/parse-metadata.test.ts @@ -3,7 +3,7 @@ import { parseMetadata } from '../../src/builtin-simulator/utils/parse-metadata' describe('parseMetadata', () => { it('parseMetadata', async () => { - console.log(parseMetadata('Div')) - console.log(parseMetadata({ componentName: 'Div' })); + const md1 = parseMetadata('Div'); + const md2 = parseMetadata({ componentName: 'Div' }); }); }); \ No newline at end of file diff --git a/packages/designer/tests/builtin-simulator/renderer.test.tsx b/packages/designer/tests/builtin-simulator/renderer.test.tsx new file mode 100644 index 000000000..6cead4122 --- /dev/null +++ b/packages/designer/tests/builtin-simulator/renderer.test.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import set from 'lodash/set'; +import cloneDeep from 'lodash/clonedeep'; +import '../fixtures/window'; +import { Editor } from '@ali/lowcode-editor-core'; +import { Project } from '../../src/project/project'; +import { Node } from '../../src/document/node/node'; +import TestRenderer from 'react-test-renderer'; +import { configure, render, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { Designer } from '../../src/designer/designer'; +import formSchema from '../fixtures/schema/form'; +import { getMockRenderer } from '../utils'; +import { isSimulatorRenderer } from '../../src/builtin-simulator/renderer'; + + +describe('renderer 测试', () => { + it('renderer', () => { + expect(isSimulatorRenderer(getMockRenderer())).toBeTruthy; + }) +}); diff --git a/packages/designer/tests/designer/builtin-hotkey.test.ts b/packages/designer/tests/designer/builtin-hotkey.test.ts new file mode 100644 index 000000000..cc010f85c --- /dev/null +++ b/packages/designer/tests/designer/builtin-hotkey.test.ts @@ -0,0 +1,202 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/clonedeep'; +import '../fixtures/window'; +import { Editor, globalContext } from '@ali/lowcode-editor-core'; +import { Designer } from '../../src/designer/designer'; +import { Project } from '../../src/project/project'; +import formSchema from '../fixtures/schema/form'; +import '../../src/designer/builtin-hotkey'; + +const editor = new Editor(); + +let designer: Designer; +beforeAll(() => { + globalContext.register(editor, Editor); +}); +beforeEach(() => { + designer = new Designer({ editor }); + designer.project.open(formSchema); +}); +afterEach(() => { + designer = null; +}); + +// keyCode 对应表:https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode +// hotkey 模块底层用的 keyCode,所以还不能用 key / code 测试 +describe('快捷键测试', () => { + it('right', () => { + const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbj')!; + firstCardNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 39 }); + document.dispatchEvent(event); + + expect(designer.currentSelection?.selected.includes('node_k1ow3cbl')).toBeTruthy; + }); + + it('left', () => { + const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbl')!; + firstCardNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 37 }); + document.dispatchEvent(event); + + expect(designer.currentSelection?.selected.includes('node_k1ow3cbj')).toBeTruthy; + }); + + it('down', () => { + const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbl')!; + firstCardNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 40 }); + document.dispatchEvent(event); + + expect(designer.currentSelection?.selected.includes('node_k1ow3cbm')).toBeTruthy; + }); + + it('up', () => { + const secondCardNode = designer.currentDocument?.getNode('node_k1ow3cbm')!; + secondCardNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 38 }); + document.dispatchEvent(event); + + expect(designer.currentSelection?.selected.includes('node_k1ow3cbl')).toBeTruthy; + }); + + // 跟右侧节点调换位置 + it('option + right', () => { + const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!; + firstButtonNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 39, altKey: true }); + document.dispatchEvent(event); + + expect(firstButtonNode.prevSibling?.getId()).toBe('node_k1ow3cbp'); + }); + + // 跟左侧节点调换位置 + it('option + left', () => { + const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + secondButtonNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 37, altKey: true }); + document.dispatchEvent(event); + + expect(secondButtonNode.nextSibling?.getId()).toBe('node_k1ow3cbn'); + }); + + // 向父级移动该节点 + it('option + up', () => { + const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + firstCardNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 38, altKey: true }); + document.dispatchEvent(event); + }); + + // 将节点移入到兄弟节点中 + it('option + up', () => { + const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + firstCardNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 40, altKey: true }); + document.dispatchEvent(event); + }); + + // 撤销 + it('command + z', async () => { + const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!; + let secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + + firstButtonNode.remove(); + expect(secondButtonNode.getParent()?.children.size).toBe(1); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + let event = new KeyboardEvent('keydown', { keyCode: 90, metaKey: true }); + document.dispatchEvent(event); + + // 重新获取一次节点,因为 documentModel.import 是全画布刷新 + secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + expect(secondButtonNode.getParent()?.children.size).toBe(2); + }); + + // 重做 + it('command + y', async () => { + const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!; + let secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + + firstButtonNode.remove(); + expect(secondButtonNode.getParent()?.children.size).toBe(1); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + let event = new KeyboardEvent('keydown', { keyCode: 90, metaKey: true }); + document.dispatchEvent(event); + + // 重新获取一次节点,因为 documentModel.import 是全画布刷新 + secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + expect(secondButtonNode.getParent()?.children.size).toBe(2); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + event = new KeyboardEvent('keydown', { keyCode: 89, metaKey: true }); + document.dispatchEvent(event); + + // 重新获取一次节点,因为 documentModel.import 是全画布刷新 + secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + expect(secondButtonNode.getParent()?.children.size).toBe(1); + }); + + it('command + c', () => { + const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + firstCardNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 67, metaKey: true }); + document.dispatchEvent(event); + }); + + it('command + v', async () => { + const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + secondButtonNode.select(); + + let event = new KeyboardEvent('keydown', { keyCode: 67, metaKey: true }); + document.dispatchEvent(event); + + event = new KeyboardEvent('keydown', { keyCode: 86, metaKey: true }); + document.dispatchEvent(event); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + // clipboard 异步,先注释 + // expect(secondButtonNode.getParent()?.children.size).toBe(3); + }); + + // 撤销所有选中 + it('escape', () => { + const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + firstCardNode.select(); + + expect(designer.currentSelection!.selected.includes('node_k1ow3cbp')).toBeTruthy; + + let event = new KeyboardEvent('keydown', { keyCode: 27 }); + document.dispatchEvent(event); + + expect(designer.currentSelection!.selected.length).toBe(0); + }); + + // 删除节点 + it('delete', () => { + const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!; + const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!; + firstButtonNode.select(); + + expect(secondButtonNode.prevSibling.id).toBe('node_k1ow3cbn'); + + let event = new KeyboardEvent('keydown', { keyCode: 46 }); + document.dispatchEvent(event); + + expect(secondButtonNode.prevSibling).toBeNull; + }); +}); diff --git a/packages/designer/tests/setting-entry/setting-prop-entry.test.ts b/packages/designer/tests/designer/setting-entry/setting-prop-entry.test.ts similarity index 89% rename from packages/designer/tests/setting-entry/setting-prop-entry.test.ts rename to packages/designer/tests/designer/setting-entry/setting-prop-entry.test.ts index 683ff6e33..8ddf63f0b 100644 --- a/packages/designer/tests/setting-entry/setting-prop-entry.test.ts +++ b/packages/designer/tests/designer/setting-entry/setting-prop-entry.test.ts @@ -1,14 +1,14 @@ import set from 'lodash/set'; import cloneDeep from 'lodash/clonedeep'; -import '../fixtures/window'; +import '../../fixtures/window'; import { Editor } from '@ali/lowcode-editor-core'; -import { Project } from '../../src/project/project'; -import { Node } from '../../src/document/node/node'; -import { Designer } from '../../src/designer/designer'; -import formSchema from '../fixtures/schema/form'; -import settingSchema from '../fixtures/schema/setting'; -import divMeta from '../fixtures/prototype/div-meta'; -import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; +import { Project } from '../../../src/project/project'; +import { Node } from '../../../src/document/node/node'; +import { Designer } from '../../../src/designer/designer'; +import formSchema from '../../../fixtures/schema/form'; +import settingSchema from '../../fixtures/schema/setting'; +import divMeta from '../../fixtures/prototype/div-meta'; +import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils'; const editor = new Editor(); diff --git a/packages/designer/tests/setting-entry/setting-top-entry.test.ts b/packages/designer/tests/designer/setting-entry/setting-top-entry.test.ts similarity index 94% rename from packages/designer/tests/setting-entry/setting-top-entry.test.ts rename to packages/designer/tests/designer/setting-entry/setting-top-entry.test.ts index 435993926..e7e5dab24 100644 --- a/packages/designer/tests/setting-entry/setting-top-entry.test.ts +++ b/packages/designer/tests/designer/setting-entry/setting-top-entry.test.ts @@ -1,14 +1,14 @@ import set from 'lodash/set'; import cloneDeep from 'lodash/clonedeep'; -import '../fixtures/window'; +import '../../fixtures/window'; import { Editor } from '@ali/lowcode-editor-core'; -import { Project } from '../../src/project/project'; -import { Node } from '../../src/document/node/node'; -import { Designer } from '../../src/designer/designer'; -import formSchema from '../fixtures/schema/form'; -import settingSchema from '../fixtures/schema/setting'; -import divMeta from '../fixtures/prototype/div-meta'; -import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; +import { Project } from '../../../src/project/project'; +import { Node } from '../../../src/document/node/node'; +import { Designer } from '../../../src/designer/designer'; +import formSchema from '../../fixtures/schema/form'; +import settingSchema from '../../fixtures/schema/setting'; +import divMeta from '../../fixtures/prototype/div-meta'; +import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils'; const editor = new Editor(); diff --git a/packages/designer/tests/fixtures/unhandled-rejection.ts b/packages/designer/tests/fixtures/unhandled-rejection.ts new file mode 100644 index 000000000..d69ffa08b --- /dev/null +++ b/packages/designer/tests/fixtures/unhandled-rejection.ts @@ -0,0 +1,7 @@ +if (!process.env.LISTENING_TO_UNHANDLED_REJECTION) { + process.on('unhandledRejection', reason => { + throw reason; + }) + // Avoid memory leak by adding too many listeners + process.env.LISTENING_TO_UNHANDLED_REJECTION = true; +} \ No newline at end of file diff --git a/packages/designer/tests/utils/bom.ts b/packages/designer/tests/utils/bom.ts new file mode 100644 index 000000000..fca5f73fc --- /dev/null +++ b/packages/designer/tests/utils/bom.ts @@ -0,0 +1,77 @@ +import { getMockRenderer } from './renderer'; + +interface MockDocument extends Document { + // open(): any; + // write(): any; + // close(): any; + // addEventListener(): any; + // removeEventListener(): any; + triggerEventListener(): any; + // createElement(): any; + // appendChild(): any; + // removeChild(): any; +} + + +const eventsMap : Map> = new Map>(); +const mockAddEventListener = jest.fn((eventName: string, cb) => { + if (!eventsMap.has(eventName)) { + eventsMap.set(eventName, new Set([cb])); + return; + } + eventsMap.get(eventName)!.add(cb); +}); + +const mockRemoveEventListener = jest.fn((eventName: string, cb) => { + if (!eventsMap.has(eventName)) return; + if (!cb) { + eventsMap.delete(eventName); + return; + } + eventsMap.get(eventName)?.delete(cb); +}); + +const mockTriggerEventListener = jest.fn((eventName: string, data: any, context: object = {}) => { + if (!eventsMap.has(eventName)) return; + for (const cb of eventsMap.get(eventName)) { + cb.call(context, data); + } +}); + +const mockCreateElement = jest.fn((tagName) => { + return { + style: {}, + appendChild() {}, + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + triggerEventListener: mockTriggerEventListener, + } +}) + +export function getMockDocument(): MockDocument { + return { + open() {}, + write() {}, + close() {}, + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + triggerEventListener: mockTriggerEventListener, + createElement: mockCreateElement, + removeChild() {}, + body: { appendChild() {}, removeChild() {} }, + }; +} + +export function getMockWindow(doc?: MockDocument) { + return { + SimulatorRenderer: getMockRenderer(), + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + triggerEventListener: mockTriggerEventListener, + document: doc || getMockDocument(), + }; +} + +export function clearEventsMap() { + eventsMap.clear(); +} \ No newline at end of file diff --git a/packages/designer/tests/utils/event.ts b/packages/designer/tests/utils/event.ts new file mode 100644 index 000000000..59f7b016e --- /dev/null +++ b/packages/designer/tests/utils/event.ts @@ -0,0 +1,8 @@ +export function getMockEvent(target, options) { + return { + target, + preventDefault() {}, + stopPropagation() {}, + ...options, + }; +} \ No newline at end of file diff --git a/packages/designer/tests/utils/index.ts b/packages/designer/tests/utils/index.ts index 70fce0af2..74eb13265 100644 --- a/packages/designer/tests/utils/index.ts +++ b/packages/designer/tests/utils/index.ts @@ -1 +1,4 @@ export { getIdsFromSchema, getNodeFromSchemaById } from '@ali/lowcode-test-mate/es/utils'; +export { getMockDocument, getMockWindow } from './bom'; +export { getMockEvent } from './event'; +export { getMockRenderer } from './renderer'; \ No newline at end of file diff --git a/packages/designer/tests/utils/renderer.ts b/packages/designer/tests/utils/renderer.ts new file mode 100644 index 000000000..256a8c651 --- /dev/null +++ b/packages/designer/tests/utils/renderer.ts @@ -0,0 +1,8 @@ +export function getMockRenderer() { + return { + isSimulatorRenderer: true, + run() { + console.log('renderer run'); + } + } +} \ No newline at end of file diff --git a/packages/utils/src/is-form-event.ts b/packages/utils/src/is-form-event.ts index 30e0bdb5f..40d652fb1 100644 --- a/packages/utils/src/is-form-event.ts +++ b/packages/utils/src/is-form-event.ts @@ -7,7 +7,7 @@ export function isFormEvent(e: KeyboardEvent | MouseEvent) { if (t.form || /^(INPUT|SELECT|TEXTAREA)$/.test(t.tagName)) { return true; } - if (/write/.test(window.getComputedStyle(t).getPropertyValue('-webkit-user-modify'))) { + if (t instanceof HTMLElement && /write/.test(window.getComputedStyle(t).getPropertyValue('-webkit-user-modify'))) { return true; } return false;