Преглед изворни кода

Feature: better error handling

Hugo Di Francesco пре 4 година
родитељ
комит
b4c0cc0134
4 измењених фајлова са 117 додато и 7 уклоњено
  1. 2 1
      examples/index.html
  2. 9 6
      src/component.js
  3. 18 0
      src/utils.js
  4. 88 0
      test/error.spec.js

+ 2 - 1
examples/index.html

@@ -48,9 +48,10 @@
             </thead>
             <tbody>
                 <tr>
-                    <td>Broken Component</td>
+                    <td>Broken Components</td>
                     <td>
                         <div x-data="some.bad.expression()">I'm a broken component</div>
+                        <button x-data x-on:click="something()">I break on click</button>
                     </td>
                 </tr>
 

+ 9 - 6
src/component.js

@@ -1,4 +1,4 @@
-import { walk, saferEval, saferEvalNoReturn, getXAttrs, debounce, convertClassStringToArray, TRANSITION_CANCELLED } from './utils'
+import { walk, tryCatch, saferEval, saferEvalNoReturn, getXAttrs, debounce, convertClassStringToArray, TRANSITION_CANCELLED } from './utils'
 import { handleForDirective } from './directives/for'
 import { handleAttributeBindingDirective } from './directives/bind'
 import { handleTextDirective } from './directives/text'
@@ -28,7 +28,9 @@ export default class Component {
             Object.defineProperty(dataExtras, `$${name}`, { get: function () { return callback(canonicalComponentElementReference) } });
         })
 
-        this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(dataExpression, dataExtras)
+        tryCatch(() => {
+            this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(dataExpression, dataExtras)
+        }, { el, expression: dataExpression })
 
         /* IE11-ONLY:START */
             // For IE11, add our magic properties to the original data for access.
@@ -351,17 +353,18 @@ export default class Component {
     }
 
     evaluateReturnExpression(el, expression, extraVars = () => {}) {
-        return saferEval(expression, this.$data, {
+        return tryCatch(() => saferEval(expression, this.$data, {
             ...extraVars(),
             $dispatch: this.getDispatchFunction(el),
-        })
+        }),
+        { el, expression })
     }
 
     evaluateCommandExpression(el, expression, extraVars = () => {}) {
-        return saferEvalNoReturn(expression, this.$data, {
+        return tryCatch(() => saferEvalNoReturn(expression, this.$data, {
             ...extraVars(),
             $dispatch: this.getDispatchFunction(el),
-        })
+        }), { el, expression })
     }
 
     getDispatchFunction (el) {

+ 18 - 0
src/utils.js

@@ -65,6 +65,24 @@ export function debounce(func, wait) {
     }
 }
 
+const handleError = (el, expression, error) => {
+    console.error(`Alpine: error in expression "${expression}" in component: `, el, `due to "${error}"`);
+    if (!isTesting()) {
+        throw error;
+    }
+}
+
+export function tryCatch(cb, { el, expression }) {
+    try {
+        const value = cb();
+        return value instanceof Promise
+            ? value.catch((e) => handleError(el, expression, e))
+            : value;
+    } catch (e) {
+        handleError(el, expression, e)
+    }
+}
+
 export function saferEval(expression, dataContext, additionalHelperVariables = {}) {
     if (typeof expression === 'function') {
         return expression.call(dataContext)

+ 88 - 0
test/error.spec.js

@@ -0,0 +1,88 @@
+import Alpine from 'alpinejs'
+import { wait } from '@testing-library/dom'
+
+global.MutationObserver = class {
+    observe() {}
+}
+
+jest.spyOn(window, 'setTimeout').mockImplementation((callback) => {
+    callback()
+})
+
+const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
+
+beforeEach(() => {
+    jest.clearAllMocks()
+})
+
+test('error in x-data eval contains element, expression and original error', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ foo: 'bar' ">
+            <span x-bind:foo="foo.bar"></span>
+        </div>
+    `
+    await expect(Alpine.start()).rejects.toThrow()
+    expect(mockConsoleError).toHaveBeenCalledWith(
+        "Alpine: error in expression \"{ foo: 'bar' \" in component: ",
+        document.querySelector('[x-data]'),
+        "due to \"SyntaxError: Unexpected token ')'\""
+    )
+})
+
+test('error in x-bind eval contains element, expression and original error', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ foo: null }">
+            <span x-bind:foo="foo.bar"></span>
+        </div>
+    `
+    await Alpine.start()
+    expect(mockConsoleError).toHaveBeenCalledWith(
+        "Alpine: error in expression \"foo.bar\" in component: ",
+        document.querySelector('[x-bind:foo]'),
+        "due to \"TypeError: Cannot read property 'bar' of null\""
+    )
+})
+
+test('error in x-model eval contains element, expression and original error', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ foo: null }">
+            <input x-model="foo.bar">
+        </div>
+    `
+    await Alpine.start()
+    expect(mockConsoleError).toHaveBeenCalledWith(
+        "Alpine: error in expression \"foo.bar\" in component: ",
+        document.querySelector('[x-model]'),
+        "due to \"TypeError: Cannot read property 'bar' of null\""
+    )
+})
+
+test('error in x-for eval contains element, expression and original error', async () => {
+    document.body.innerHTML = `
+        <div x-data="{}">
+            <template x-for="element in foo">
+                <span x-text="element"></span>
+            </template>
+        </div>
+    `
+    await expect(Alpine.start()).rejects.toThrow()
+    expect(mockConsoleError).toHaveBeenCalledWith(
+        "Alpine: error in expression \"foo\" in component: ",
+        document.querySelector('[x-for]'),
+        "due to \"ReferenceError: foo is not defined\""
+    )
+})
+
+test('error in x-on eval contains element, expression and original error', async () => {
+    document.body.innerHTML = `
+        <div
+            x-data="{hello: null}"
+            x-on:click="hello.world"
+        ></div>
+    `
+    await Alpine.start()
+    document.querySelector('div').click()
+    await wait(() => {
+        expect(mockConsoleError).toHaveBeenCalled()
+    })
+})