, unknown>;
export function getComponentTypeForElement>(
@@ -79,9 +140,6 @@ export function getComponentForElement(element: ElementNode): React.ReactNode {
if (isHTMLElementNode(newElement)) {
return HTMLElementView({ element: newElement });
}
- if (isSpectrumElementNode(newElement)) {
- return SpectrumElementView({ element: newElement });
- }
if (isIconElementNode(newElement)) {
return IconElementView({ element: newElement });
}
@@ -89,7 +147,16 @@ export function getComponentForElement(element: ElementNode): React.ReactNode {
const Component = getComponentTypeForElement(newElement);
if (Component != null) {
- return ;
+ const props =
+ shouldWrapTextChildren.has(newElement[ELEMENT_KEY]) &&
+ newElement.props?.children != null
+ ? {
+ ...newElement.props,
+ children: wrapTextChildren(newElement.props.children),
+ }
+ : newElement.props;
+
+ return ;
}
}
@@ -115,3 +182,112 @@ export function getPreservedData(
Object.entries(oldData).filter(([key]) => PRESERVED_DATA_KEYS_SET.has(key))
);
}
+
+/**
+ * Wraps a callable returned by the server so any returned callables are also wrapped.
+ * The callable will also be added to the finalization registry so it can be cleaned up
+ * when there are no more strong references to the callable.
+ * @param jsonClient The JSON client to send callable requests to
+ * @param callableId The callableId to return a wrapped callable for
+ * @param registry The finalization registry to register the callable with.
+ * @returns A wrapped callable that will automatically wrap any nested callables returned by the server
+ */
+export function wrapCallable(
+ jsonClient: JSONRPCServerAndClient,
+ callableId: string,
+ registry: FinalizationRegistry
+): (...args: unknown[]) => Promise {
+ const callable = async (...args: unknown[]) => {
+ log.debug2(`Callable ${callableId} called`, args);
+ const resultString = await jsonClient.request('callCallable', [
+ callableId,
+ args,
+ ]);
+
+ log.debug2(`Callable ${callableId} result string`, resultString);
+
+ try {
+ // Do NOT add anything that logs result
+ // It creates a strong ref to the result object in the console logs
+ // As a result, any returned callables will never be GC'd and the finalization registry will never clean them up
+ const result = JSON.parse(resultString, (key, value) => {
+ if (isCallableNode(value)) {
+ const nestedCallable = wrapCallable(
+ jsonClient,
+ value[CALLABLE_KEY],
+ registry
+ );
+ return nestedCallable;
+ }
+ return value;
+ });
+
+ return result;
+ } catch {
+ throw new Error(`Error parsing callable result: ${resultString}`);
+ }
+ };
+
+ registry.register(callable, callableId, callable);
+
+ return callable;
+}
+
+/**
+ * Get the name of an error type
+ * @param error Name of an error
+ * @returns The name of the error
+ */
+export function getErrorName(error: unknown): string {
+ if (isWidgetError(error)) {
+ return error.name;
+ }
+ return 'Unknown error';
+}
+
+/**
+ * Get the message of an error
+ * @param error Error object
+ * @returns The error message
+ */
+export function getErrorMessage(error: unknown): string {
+ if (isWidgetError(error)) {
+ return error.message.trim();
+ }
+ return 'Unknown error';
+}
+
+/**
+ * Get the short message of an error. Just the first line of the error message.
+ * @param error Error object
+ * @returns The error short message
+ */
+export function getErrorShortMessage(error: unknown): string {
+ const message = getErrorMessage(error);
+ const lines = message.split('\n');
+ return lines[0].trim();
+}
+
+/**
+ * Get the stack trace of an error
+ * @param error Error object
+ * @returns The error stack trace
+ */
+export function getErrorStack(error: unknown): string {
+ if (isWidgetError(error)) {
+ return error.stack ?? '';
+ }
+ return '';
+}
+
+/**
+ * Get the action from an error object if it exists
+ * @param error Error object
+ * @returns The action from the error, if it exists
+ */
+export function getErrorAction(error: unknown): WidgetAction | null {
+ if (isWidgetError(error)) {
+ return error.action ?? null;
+ }
+ return null;
+}
diff --git a/tests/app.d/tests.app b/tests/app.d/tests.app
index 43694c37f..6fe8afd7f 100644
--- a/tests/app.d/tests.app
+++ b/tests/app.d/tests.app
@@ -6,3 +6,4 @@ name=Plugins Test Application
file_0=express.py
file_1=matplotlib.py
file_2=ui.py
+file_3=ui_render_all.py
\ No newline at end of file
diff --git a/tests/app.d/ui.py b/tests/app.d/ui.py
index 568053c75..290ec12d8 100644
--- a/tests/app.d/ui.py
+++ b/tests/app.d/ui.py
@@ -26,7 +26,7 @@ def ui_boom_counter_component():
value, set_value = ui.use_state(0)
if value > 1:
- raise ValueError("BOOM!")
+ raise ValueError("BOOM! Value too big.")
return ui.button(f"Count is {value}", on_press=lambda _: set_value(value + 1))
diff --git a/tests/app.d/ui_render_all.py b/tests/app.d/ui_render_all.py
new file mode 100644
index 000000000..3d138a3c5
--- /dev/null
+++ b/tests/app.d/ui_render_all.py
@@ -0,0 +1,137 @@
+# This file is used as a high level way to ensure all UI components render
+# without error. We should add more robust tests that reflect all of our
+# examples as suggested by #417
+from deephaven import ui, empty_table
+
+icon_names = ["vsAccount"]
+columns = [
+ "Id=new Integer(i)",
+ "Display=new String(`Display `+i)",
+ "Description=new String(`Description `+i)",
+ "Icon=(String) icon_names[0]",
+]
+_column_types = empty_table(20).update(columns)
+
+_item_table_source_with_icons = ui.item_table_source(
+ _column_types,
+ key_column="Id",
+ label_column="Display",
+ icon_column="Icon",
+)
+
+_item_table_source_with_action_group = ui.item_table_source(
+ _column_types,
+ key_column="Id",
+ label_column="Display",
+ icon_column="Icon",
+ actions=ui.list_action_group(
+ ui.item("Edit"),
+ ui.item("Delete"),
+ ),
+)
+
+_item_table_source_with_action_menu = ui.item_table_source(
+ _column_types,
+ key_column="Id",
+ label_column="Display",
+ icon_column="Icon",
+ actions=ui.list_action_menu(
+ ui.item("Edit"),
+ ui.item("Delete"),
+ ),
+)
+
+
+@ui.component
+def ui_components():
+ return (
+ ui.action_button("Action Button"),
+ ui.action_group("Aaa", "Bbb", "Ccc"),
+ ui.action_menu("Aaa", "Bbb", "Ccc"),
+ ui.button_group(ui.button("One"), ui.button("Two")),
+ ui.button("Button"),
+ ui.checkbox("Checkbox"),
+ ui.column("Column child A", "Column child B", "Column child C"),
+ # TODO: #201 ui.combo_box("Combo Box"),
+ ui.content("Content"),
+ ui.contextual_help("Contextual Help"),
+ # TODO: #367 ui.date_picker("Date Picker"),
+ ui.flex("Flex default child A", "Flex default child B"),
+ ui.flex("Flex column child A", "Flex column child B", direction="column"),
+ ui.form("Form"),
+ ui.fragment("Fragment"),
+ ui.grid("Grid A", "Grid B"),
+ ui.heading("Heading"),
+ ui.icon("vsSymbolMisc"),
+ # TODO: #526 ui.icon_wrapper("TODO: fix this"),
+ ui.illustrated_message(ui.icon("vsSymbolMisc"), "Illustrated Message"),
+ ui.list_view(
+ _item_table_source_with_action_group,
+ aria_label="List View - List action group",
+ min_height="size-1600",
+ ),
+ ui.list_view(
+ _item_table_source_with_action_menu,
+ aria_label="List View - List action menu",
+ min_height="size-1600",
+ ),
+ ui.number_field("Number Field", aria_label="Number field"),
+ ui.picker(
+ "Aaa",
+ "Bbb",
+ ui.section("Ccc", "Ddd", title="Section A"),
+ aria_label="Picker with Section",
+ ),
+ ui.picker(
+ _item_table_source_with_icons, aria_label="Picker", default_selected_key=15
+ ),
+ ui.radio_group(
+ ui.radio("One", value="one"),
+ ui.radio("Two", value="two"),
+ label="Radio Group",
+ orientation="HORIZONTAL",
+ ),
+ ui.range_slider(default_value={"start": 10, "end": 99}, label="Range Slider"),
+ ui.row("Row child A", "Row child B"),
+ ui.slider(
+ label="Slider",
+ default_value=40,
+ min_value=-100.0,
+ max_value=100.0,
+ step=0.1,
+ ),
+ ui.switch("Switch"),
+ # TODO: #191
+ # ui.tab_list("Tab List"),
+ # ui.tab_panels("Tab Panels"),
+ # ui.tabs("Tabs"),
+ ui.text("Text"),
+ ui.text_field(
+ ui.icon("vsSymbolMisc"), default_value="Text Field", label="Text Field"
+ ),
+ ui.toggle_button(
+ ui.icon("vsBell"),
+ "By Exchange",
+ ),
+ ui.view("View"),
+ )
+
+
+@ui.component
+def ui_html_elements():
+ # TODO: render other ui.html elements #417
+ ui.html.div("div"),
+
+
+_my_components = ui_components()
+_my_html_elements = ui_html_elements()
+
+ui_render_all = ui.dashboard(
+ ui.stack(
+ ui.panel(
+ ui.table(_column_types),
+ ui.grid(_my_components, _my_html_elements, columns=["1fr", "1fr", "1fr"]),
+ title="Panel B",
+ ),
+ )
+)
diff --git a/tests/ui.spec.ts b/tests/ui.spec.ts
index 30c387a08..232233169 100644
--- a/tests/ui.spec.ts
+++ b/tests/ui.spec.ts
@@ -1,34 +1,39 @@
import { expect, test } from '@playwright/test';
import { openPanel, gotoPage } from './utils';
+const selector = {
+ REACT_PANEL_VISIBLE: '.dh-react-panel:visible',
+ REACT_PANEL_OVERLAY: '.dh-react-panel-overlay',
+};
+
test('UI loads', async ({ page }) => {
await gotoPage(page, '');
- await openPanel(page, 'ui_component', '.dh-react-panel');
- await expect(page.locator('.dh-react-panel')).toHaveScreenshot();
+ await openPanel(page, 'ui_component', selector.REACT_PANEL_VISIBLE);
+ await expect(page.locator(selector.REACT_PANEL_VISIBLE)).toHaveScreenshot();
});
test('boom component shows an error in a panel', async ({ page }) => {
await gotoPage(page, '');
- await openPanel(page, 'ui_boom', '.dh-react-panel');
- await expect(page.locator('.dh-react-panel')).toBeVisible();
+ await openPanel(page, 'ui_boom', selector.REACT_PANEL_VISIBLE);
+ await expect(page.locator(selector.REACT_PANEL_VISIBLE)).toBeVisible();
await expect(
- page.locator('.dh-react-panel').getByText('Exception', { exact: true })
+ page
+ .locator(selector.REACT_PANEL_VISIBLE)
+ .getByText('Exception', { exact: true })
).toBeVisible();
await expect(
- page
- .locator('.dh-react-panel')
- .getByText('BOOM! Traceback (most recent call last)')
+ page.locator(selector.REACT_PANEL_VISIBLE).getByText('BOOM!')
).toBeVisible();
- await expect(page.locator('.dh-react-panel-overlay')).not.toBeVisible();
+ await expect(page.locator(selector.REACT_PANEL_OVERLAY)).not.toBeVisible();
});
test('boom counter component shows error overlay after clicking the button twice', async ({
page,
}) => {
await gotoPage(page, '');
- await openPanel(page, 'ui_boom_counter', '.dh-react-panel');
+ await openPanel(page, 'ui_boom_counter', selector.REACT_PANEL_VISIBLE);
- const panelLocator = page.locator('.dh-react-panel');
+ const panelLocator = page.locator(selector.REACT_PANEL_VISIBLE);
let btn = await panelLocator.getByRole('button', { name: 'Count is 0' });
await expect(btn).toBeVisible();
@@ -38,12 +43,16 @@ test('boom counter component shows error overlay after clicking the button twice
await expect(btn).toBeVisible();
btn.click();
- const overlayLocator = page.locator('.dh-react-panel-overlay');
+ const overlayLocator = page.locator(selector.REACT_PANEL_OVERLAY);
await expect(
overlayLocator.getByText('ValueError', { exact: true })
).toBeVisible();
- await expect(
- overlayLocator.getByText('BOOM! Traceback (most recent call last)')
- ).toBeVisible();
+ await expect(overlayLocator.getByText('BOOM! Value too big.')).toBeVisible();
+});
+
+test('UI all components render', async ({ page }) => {
+ await gotoPage(page, '');
+ await openPanel(page, 'ui_render_all', selector.REACT_PANEL_VISIBLE);
+ await expect(page.locator(selector.REACT_PANEL_VISIBLE)).toHaveScreenshot();
});
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-chromium-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-chromium-linux.png
new file mode 100644
index 000000000..497c29423
Binary files /dev/null and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-chromium-linux.png differ
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-firefox-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-firefox-linux.png
new file mode 100644
index 000000000..84b32377c
Binary files /dev/null and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-firefox-linux.png differ
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-webkit-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-webkit-linux.png
new file mode 100644
index 000000000..456fb28df
Binary files /dev/null and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-webkit-linux.png differ