diff --git a/packages/toolpad-app/src/appDom/index.ts b/packages/toolpad-app/src/appDom/index.ts index a6c89f4da9a..1ea4ee35522 100644 --- a/packages/toolpad-app/src/appDom/index.ts +++ b/packages/toolpad-app/src/appDom/index.ts @@ -14,7 +14,7 @@ import { ConnectionStatus, AppTheme } from '../types'; import { omit, update, updateOrCreate } from '../utils/immutability'; import { camelCase, generateUniqueString, removeDiacritics } from '../utils/strings'; import { ExactEntriesOf, Maybe } from '../utils/types'; -import { filterValues } from '../utils/collections'; +import { mapProperties } from '../utils/collections'; export const CURRENT_APPDOM_VERSION = 2; @@ -614,6 +614,23 @@ export function setNodeProp, + Prop extends keyof Node[Namespace] & string, +>(node: Node, namespace: Namespace, prop: Prop, value: Node[Namespace][Prop] | null): Node { + if (value) { + return update(node, { + [namespace]: updateOrCreate((node as Node)[namespace], { + [prop]: value, + } as any) as Partial, + } as Partial); + } + return update(node, { + [namespace]: omit(node[namespace], prop) as Partial, + } as Partial); +} + export function setNodeNamespacedProp< Node extends AppDomNode, Namespace extends PropNamespaces, @@ -875,16 +892,31 @@ export interface RenderTree { version?: number; } +const frontendNodes = new Set(RENDERTREE_NODES); +function createRenderTreeNode(node: AppDomNode): RenderTreeNode | null { + if (!frontendNodes.has(node.type)) { + return null; + } + + if (isQuery(node) || isMutation(node)) { + node = setNamespacedProp(node, 'attributes', 'query', null); + } + + return node as RenderTreeNode; +} + /** * We need to make sure no secrets end up in the frontend html, so let's only send the * nodes that we need to build frontend, and that we know don't contain secrets. * TODO: Would it make sense to create a separate datastructure that represents the render tree? */ export function createRenderTree(dom: AppDom): RenderTree { - const frontendNodes = new Set(RENDERTREE_NODES); return { ...dom, - nodes: filterValues(dom.nodes, (node) => frontendNodes.has(node.type)) as RenderTreeNodes, + nodes: mapProperties(dom.nodes, ([id, node]) => { + const rendernode = createRenderTreeNode(node); + return rendernode ? [id, rendernode] : null; + }), }; } diff --git a/test/integration/propControls/index.spec.ts b/test/integration/propControls/index.spec.ts index c483811fa11..a8675b061d0 100644 --- a/test/integration/propControls/index.spec.ts +++ b/test/integration/propControls/index.spec.ts @@ -1,25 +1,9 @@ -import { test, expect, Locator } from '@playwright/test'; +import { test, expect } from '@playwright/test'; import { ToolpadHome } from '../../models/ToolpadHome'; import { ToolpadEditor } from '../../models/ToolpadEditor'; import clickCenter from '../../utils/clickCenter'; import domInput from './domInput.json'; -async function getPropControlInputLocator(editorModel: ToolpadEditor, inputPropName: string) { - const propControlLabelHandle = await editorModel.componentEditor - .locator(`label:has-text("${inputPropName}")`) - .elementHandle(); - const propControlLabelFor = await propControlLabelHandle?.getAttribute('for'); - - return editorModel.componentEditor.locator(`input[id="${propControlLabelFor}"]`); -} - -async function getInputElementLabelLocator(editorModel: ToolpadEditor, inputLocator: Locator) { - const inputHandle = await inputLocator.elementHandle(); - const inputId = await inputHandle?.getAttribute('id'); - - return editorModel.appCanvas.locator(`label[for="${inputId}"]`); -} - test('can control component prop values in properties control panel', async ({ page, browserName, @@ -40,35 +24,28 @@ test('can control component prop values in properties control panel', async ({ const firstInputLocator = canvasInputLocator.first(); await clickCenter(page, firstInputLocator); - await editorModel.componentEditor.waitFor(); + await editorModel.componentEditor.waitFor({ state: 'visible' }); + + const labelControlInput = editorModel.componentEditor.getByLabel('label', { exact: true }); - const labelControlInputValue = await editorModel.componentEditor - .locator(`label:text-is("label")`) - .inputValue(); + const labelControlInputValue = await labelControlInput.inputValue(); expect(labelControlInputValue).toBe('textField1'); // Change component prop values directly - const TEST_VALUE_1 = 'value1'; - - const getValueControlInputValue = async () => - editorModel.componentEditor.locator(`label:text-is("value")`).inputValue(); - - expect(await getValueControlInputValue()).not.toBe(TEST_VALUE_1); + const valueControl = editorModel.componentEditor.getByLabel('value', { exact: true }); + expect(await valueControl.inputValue()).not.toBe(TEST_VALUE_1); await firstInputLocator.fill(TEST_VALUE_1); - expect(await getValueControlInputValue()).toBe(TEST_VALUE_1); + expect(await valueControl.inputValue()).toBe(TEST_VALUE_1); // Change component prop values through controls - - const firstInputLabelLocator = await getInputElementLabelLocator(editorModel, firstInputLocator); const TEST_VALUE_2 = 'value2'; + const inputByLabel = editorModel.appCanvas.getByLabel(TEST_VALUE_2, { exact: true }); + await expect(inputByLabel).toHaveCount(0); + await labelControlInput.click(); + await labelControlInput.fill(''); + await labelControlInput.fill(TEST_VALUE_2); - await expect(firstInputLabelLocator).not.toHaveText(TEST_VALUE_2); - - const labelControlInputLocator = await getPropControlInputLocator(editorModel, 'label'); - await labelControlInputLocator.fill(''); - await labelControlInputLocator.fill(TEST_VALUE_2); - - await expect(firstInputLabelLocator).toHaveText(TEST_VALUE_2); + await inputByLabel.waitFor({ state: 'visible' }); });