diff --git a/zk/src/main/java/org/zkoss/zk/ui/http/WpdExtendlet.java b/zk/src/main/java/org/zkoss/zk/ui/http/WpdExtendlet.java index 42301e6efd..ce75a0d619 100644 --- a/zk/src/main/java/org/zkoss/zk/ui/http/WpdExtendlet.java +++ b/zk/src/main/java/org/zkoss/zk/ui/http/WpdExtendlet.java @@ -688,6 +688,8 @@ else if (WebApps.getFeature("pe")) sb.append("ta:1,"); if (config.isDebugJS()) sb.append("dj:1,"); + if (config.isSendClientErrors()) + sb.append("sce:1,"); if (config.isKeepDesktopAcrossVisits()) sb.append("kd:1,"); if (config.getPerformanceMeter() != null) diff --git a/zk/src/main/java/org/zkoss/zk/ui/impl/DesktopImpl.java b/zk/src/main/java/org/zkoss/zk/ui/impl/DesktopImpl.java index eaac5a31a2..a9ec58335d 100644 --- a/zk/src/main/java/org/zkoss/zk/ui/impl/DesktopImpl.java +++ b/zk/src/main/java/org/zkoss/zk/ui/impl/DesktopImpl.java @@ -843,7 +843,7 @@ public void service(AuRequest request, boolean everError) { } else if ("error".equals(cmd)) { final Map data = request.getData(); if (data != null) - log.error(this + " client error: " + data.get("message")); + log.error("Client encountered an error at {}:\n{}", data.get("href"), data.get("message")); } else if ("fallbackServerPushClass".equals(cmd)) { try { getDevice().setServerPushClass(Classes.forNameByThread("org.zkoss.zkex.ui.comet.CometServerPush")); diff --git a/zk/src/main/java/org/zkoss/zk/ui/sys/ConfigParser.java b/zk/src/main/java/org/zkoss/zk/ui/sys/ConfigParser.java index 73dfd1899e..e9183ab511 100644 --- a/zk/src/main/java/org/zkoss/zk/ui/sys/ConfigParser.java +++ b/zk/src/main/java/org/zkoss/zk/ui/sys/ConfigParser.java @@ -348,6 +348,7 @@ else if ("listener".equals(elnm)) { // resend-delay // debug-js // enable-source-map + // send-client-errors // auto-resend-timeout parseClientConfig(config, el); @@ -766,6 +767,11 @@ private static void parseClientConfig(Configuration config, Element conf) { if (s != null) config.enableSourceMap(!"false".equals(s)); + //F100-ZK-5135: add new config for whether to send client errors to the server + s = conf.getElementValue("send-client-errors", true); + if (s != null) + config.setSendClientErrors(!"false".equals(s)); + //F70-ZK-2495: add new config to customize crash script s = conf.getElementValue("init-crash-script", true); if (s != null) diff --git a/zk/src/main/java/org/zkoss/zk/ui/util/Configuration.java b/zk/src/main/java/org/zkoss/zk/ui/util/Configuration.java index 368bd6839c..998840d2a0 100644 --- a/zk/src/main/java/org/zkoss/zk/ui/util/Configuration.java +++ b/zk/src/main/java/org/zkoss/zk/ui/util/Configuration.java @@ -189,6 +189,7 @@ public class Configuration { private boolean _sourceMapEnabled = false; private boolean _historyStateEnabled = true; + private boolean _sendClientErrors = false; /** Constructor. */ @@ -3265,4 +3266,21 @@ public boolean isHistoryStateEnabled() { return _historyStateEnabled; } + /** + * Sets whether to send client errors to the server. + *

Default: false.

+ * @since 10.0.0 + */ + public void setSendClientErrors(boolean send) { + _sendClientErrors = send; + } + + /** + * Returns whether to send client errors to the server. + *

Default: false.

+ * @since 10.0.0 + */ + public boolean isSendClientErrors() { + return _sendClientErrors; + } } diff --git a/zk/src/main/resources/web/js/zk/widget.ts b/zk/src/main/resources/web/js/zk/widget.ts index 4c16191ab6..a63782195a 100755 --- a/zk/src/main/resources/web/js/zk/widget.ts +++ b/zk/src/main/resources/web/js/zk/widget.ts @@ -7171,6 +7171,7 @@ export namespace widget_global { case 'td': zk.tipDelay = val as number; break; case 'art': zk.resendTimeout = val as number; break; case 'dj': zk.debugJS = val as boolean; break; + case 'sce': zk.sendClientErrors = val as boolean; break; case 'kd': zk.keepDesktop = val as boolean; break; case 'pf': zk.pfmeter = val as boolean; break; case 'ta': zk.timerAlive = val as boolean; break; diff --git a/zk/src/main/resources/web/js/zk/zk.ts b/zk/src/main/resources/web/js/zk/zk.ts index 0b130ab195..cead704c27 100644 --- a/zk/src/main/resources/web/js/zk/zk.ts +++ b/zk/src/main/resources/web/js/zk/zk.ts @@ -1197,17 +1197,23 @@ _zk._noESC = 0; //# of disableESC being called (also used by mount.js) * ```ts * zk.error('Oops! Something wrong:('); * ``` - * @param msg - the error message + * @param err - the error or error message * @param silent - only show error box + * @see {@link errorPush} * @see {@link errorDismiss} * @see {@link log} * @see {@link stamp} */ -_zk.error = function (msg: string, silent?: boolean): void { +_zk.error = function (err: Error | string, silent?: boolean): void { + const msg = err instanceof Error ? err.message : err, + stack = err instanceof Error ? err.stack : undefined; if (!silent) { - zAu.send(new zk.Event(zk.Desktop._dt, 'error', {message: msg}, {ignorable: true}), 800); + if (stack) + _zk.debugLog(stack); + if (_zk.sendClientErrors) + zAu.send(new zk.Event(zk.Desktop._dt, 'error', {href: document.location.href, message: stack ?? msg}, {ignorable: true}), 800); } - _zk._Erbx.push(msg); + _zk.errorPush(msg); }; //DEBUG// /** @@ -1222,6 +1228,19 @@ _zk.error = function (msg: string, silent?: boolean): void { _zk.debugLog = function (msg: string): void { if (_zk.debugJS) console.log(msg); // eslint-disable-line no-console }; +/** + * Push an error message to the error box. + * Example: + * ```ts + * zk.errorPush('Oops! Something wrong:('); + * ``` + * @param msg - the error message + * @see {@link zk.error} + * @since 10.0.0 + */ +_zk.errorPush = function (msg: string): void { + _zk._Erbx.push(msg); +}; /** Closes all error messages shown by {@link error}. * Example: * ```ts @@ -2154,6 +2173,8 @@ declare namespace _zk { export let processMask: boolean | undefined; // eslint-disable-next-line zk/preferStrictBooleanType export let debugJS: boolean | undefined; + // eslint-disable-next-line zk/preferStrictBooleanType + export let sendClientErrors: boolean | undefined; export let updateURI: string | undefined; export let resourceURI: string | undefined; export let contextURI: string | undefined; diff --git a/zkdoc/release-note b/zkdoc/release-note index 3d93841b50..6142224983 100644 --- a/zkdoc/release-note +++ b/zkdoc/release-note @@ -11,6 +11,7 @@ ZK 10.0.0 ZK-5512: Support for Listening to Space Key Pressed ZK-5096: Consider deprecate Anchorlayout and Anchorchildren ZK-4988: zEmbedded support for websocket + ZK-5135: Make a client error more helpful for debug * Bugs ZK-5393: Update ZK jars to jakarta-friendly uploads @@ -41,8 +42,8 @@ ZK 10.0.0 simplifying the source to enable embedded mode. + The setContent() method of Comboitem, Menu, and Navitem now only accepts a safe HTML content. i.e. The content will be sanitized before rendering, please don't use JavaScript content. - + When multiple zEmbedded components with WebSocket enabled are present on a page, - the WebSocket endpoint of the last loaded zEmbedded component will be used. + + zEmbedded supports WebSocket under the condition that only one ZK page can be embedded into a non-ZK page + when WebSocket is enabled. -------- ZK 10.0.0-Beta Oct 17, 2023 diff --git a/zktest/src/main/webapp/WEB-INF/zk.xml b/zktest/src/main/webapp/WEB-INF/zk.xml index a451f95c34..2e2a4b3afb 100644 --- a/zktest/src/main/webapp/WEB-INF/zk.xml +++ b/zktest/src/main/webapp/WEB-INF/zk.xml @@ -50,6 +50,7 @@ Copyright (C) 2006 Potix Corporation. All Rights Reserved. true false + false + + + + + \ No newline at end of file diff --git a/zktest/src/main/webapp/test2/config.properties b/zktest/src/main/webapp/test2/config.properties index e24646b61e..0329111979 100644 --- a/zktest/src/main/webapp/test2/config.properties +++ b/zktest/src/main/webapp/test2/config.properties @@ -3743,6 +3743,7 @@ F86-ZK-4235.zul=A,E,datefmt,library-property ##manually##F100-ZK-5048.zul=A,E,MVVM,DebuggerFactory,Log ##zats##F100-ZK-5512.zul=A,E,CtrlKeys,Space #manually##F100-ZK-4988.html=A,E,Embedded,WebSocket +##zats##F100-ZK-5135.zul=A,E,debugJS,sendClientErrors # Complex Test Case # diff --git a/zktest/src/test/java/org/zkoss/zktest/zats/test2/F100_ZK_5135Test.java b/zktest/src/test/java/org/zkoss/zktest/zats/test2/F100_ZK_5135Test.java new file mode 100644 index 0000000000..919a65153f --- /dev/null +++ b/zktest/src/test/java/org/zkoss/zktest/zats/test2/F100_ZK_5135Test.java @@ -0,0 +1,62 @@ +/* F100_ZK_5135Test.java + + Purpose: + + Description: + + History: + Mon Jan 15 17:01:07 CST 2024, Created by rebeccalai + +Copyright (C) 2024 Potix Corporation. All Rights Reserved. +*/ +package org.zkoss.zktest.zats.test2; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.openqa.selenium.logging.LogType; +import org.slf4j.Logger; + +import org.zkoss.test.webdriver.ExternalZkXml; +import org.zkoss.test.webdriver.ForkJVMTestOnly; +import org.zkoss.test.webdriver.WebDriverTestCase; +import org.zkoss.zk.ui.impl.DesktopImpl; + +@ForkJVMTestOnly +public class F100_ZK_5135Test extends WebDriverTestCase { + @RegisterExtension + public static final ExternalZkXml CONFIG = new ExternalZkXml("/test2/F100-ZK-5135-zk.xml"); + + @Test + public void test() throws Exception { + Logger logger = mock(Logger.class); + setFinalStatic(DesktopImpl.class.getDeclaredField("log"), logger); + connect(); + waitResponse(); + assertTrue(jq(".z-error").text().contains("custom error message")); + // check browser log + driver.manage().logs().get(LogType.BROWSER).getAll().stream().findFirst().ifPresent(log -> + assertTrue(log.getMessage().contains("SimpleConstraint._init"))); + // check server log + verify(logger, atLeastOnce()).error(anyString(), contains("F100-ZK-5135.zul"), + contains("SimpleConstraint._init")); + } + + // https://stackoverflow.com/a/30703932 + private static void setFinalStatic(Field field, Object newValue) throws Exception { + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, newValue); + } +} diff --git a/zul/src/main/resources/web/js/zul/inp/SimpleConstraint.ts b/zul/src/main/resources/web/js/zul/inp/SimpleConstraint.ts index 5ff33d5799..c02e452c46 100644 --- a/zul/src/main/resources/web/js/zul/inp/SimpleConstraint.ts +++ b/zul/src/main/resources/web/js/zul/inp/SimpleConstraint.ts @@ -126,7 +126,7 @@ export class SimpleConstraint extends zk.Object { try { this._regex = new RegExp(k >= 0 ? cst.substring(j, k) : cst.substring(j), regexFlags || 'g'); } catch (e) { - zk.error((e as Error).message || e as string); + zk.error(e as Error | string); } this._cstArr[this._cstArr.length] = 'regex';