|
| 1 | +import { |
| 2 | + dynamic, |
| 3 | + EditableValueBuilder, |
| 4 | + list, |
| 5 | + listAttribute, |
| 6 | + obj, |
| 7 | + SelectionSingleValueBuilder |
| 8 | +} from "@mendix/widget-plugin-test-utils"; |
| 9 | +import { ListAttributeValue, ObjectItem } from "mendix"; |
| 10 | +import { ComboboxContainerProps } from "../../../../typings/ComboboxProps"; |
| 11 | +import { DatabaseSingleSelectionSelector } from "../DatabaseSingleSelectionSelector"; |
| 12 | + |
| 13 | +type PropsOverrides = { |
| 14 | + items: ObjectItem[]; |
| 15 | + values: Map<string, string>; |
| 16 | + targetValue?: string; |
| 17 | + selection?: ReturnType<SelectionSingleValueBuilder["build"]>; |
| 18 | +}; |
| 19 | + |
| 20 | +function buildProps({ items, values, targetValue, selection }: PropsOverrides): ComboboxContainerProps { |
| 21 | + const valueAttr = listAttribute<string>(item => values.get(item.id) ?? "") as ListAttributeValue<string | Big>; |
| 22 | + (valueAttr as unknown as { id: string }).id = "valueAttrId" as any; |
| 23 | + |
| 24 | + const captionAttr = listAttribute<string>(item => `caption_${item.id}`); |
| 25 | + (captionAttr as unknown as { id: string }).id = "captionAttrId" as any; |
| 26 | + |
| 27 | + const targetAttr = |
| 28 | + targetValue === undefined |
| 29 | + ? new EditableValueBuilder<string>().build() |
| 30 | + : new EditableValueBuilder<string>().withValue(targetValue).build(); |
| 31 | + |
| 32 | + return { |
| 33 | + name: "comboBox", |
| 34 | + id: "comboBox1", |
| 35 | + source: "database", |
| 36 | + optionsSourceType: "association", |
| 37 | + optionsSourceDatabaseDataSource: list(items), |
| 38 | + optionsSourceDatabaseItemSelection: selection ?? new SelectionSingleValueBuilder().build(), |
| 39 | + optionsSourceDatabaseCaptionType: "attribute", |
| 40 | + optionsSourceDatabaseCaptionAttribute: captionAttr, |
| 41 | + optionsSourceDatabaseValueAttribute: valueAttr, |
| 42 | + databaseAttributeString: targetAttr as any, |
| 43 | + emptyOptionText: dynamic("Select..."), |
| 44 | + optionsSourceDatabaseCustomContentType: "no", |
| 45 | + optionsSourceAssociationCustomContentType: "no", |
| 46 | + staticDataSourceCustomContentType: "no", |
| 47 | + optionsSourceAssociationCaptionType: "attribute", |
| 48 | + clearable: true, |
| 49 | + filterType: "contains", |
| 50 | + lazyLoading: false, |
| 51 | + loadingType: "spinner", |
| 52 | + customEditability: "default", |
| 53 | + customEditabilityExpression: dynamic(false), |
| 54 | + filterInputDebounceInterval: 200, |
| 55 | + selectedItemsStyle: "text", |
| 56 | + readOnlyStyle: "bordered", |
| 57 | + selectionMethod: "checkbox", |
| 58 | + selectAllButton: false, |
| 59 | + selectAllButtonCaption: dynamic("Select All"), |
| 60 | + ariaRequired: dynamic(true), |
| 61 | + showFooter: false, |
| 62 | + selectedItemsSorting: "none", |
| 63 | + attributeEnumeration: new EditableValueBuilder<string>().build(), |
| 64 | + attributeBoolean: new EditableValueBuilder<boolean>().build(), |
| 65 | + attributeAssociation: undefined as any, |
| 66 | + staticAttribute: new EditableValueBuilder<string>().build(), |
| 67 | + optionsSourceStaticDataSource: [] |
| 68 | + } as ComboboxContainerProps; |
| 69 | +} |
| 70 | + |
| 71 | +describe("DatabaseSingleSelectionSelector.updateProps — external target-attribute changes", () => { |
| 72 | + const optionA = obj("A"); |
| 73 | + const optionB = obj("B"); |
| 74 | + const items = [optionA, optionB]; |
| 75 | + const values = new Map<string, string>([ |
| 76 | + [optionA.id, "v1"], |
| 77 | + [optionB.id, "v2"] |
| 78 | + ]); |
| 79 | + |
| 80 | + it("resolves currentId from targetAttribute.value on initial updateProps", () => { |
| 81 | + const selector = new DatabaseSingleSelectionSelector({ filterInputDebounceInterval: 200 }); |
| 82 | + selector.updateProps(buildProps({ items, values, targetValue: "v1" })); |
| 83 | + |
| 84 | + expect(selector.currentId).toBe(optionA.id); |
| 85 | + }); |
| 86 | + |
| 87 | + it("refreshes currentId when targetAttribute.value changes externally after a selection exists", () => { |
| 88 | + // WC-3355: without this behavior, an external value change (e.g. from a microflow) |
| 89 | + // leaves currentId pointing at the stale option. |
| 90 | + const selector = new DatabaseSingleSelectionSelector({ filterInputDebounceInterval: 200 }); |
| 91 | + const selection = new SelectionSingleValueBuilder().build(); |
| 92 | + |
| 93 | + selector.updateProps(buildProps({ items, values, targetValue: "v1", selection })); |
| 94 | + expect(selector.currentId).toBe(optionA.id); |
| 95 | + |
| 96 | + selector.updateProps(buildProps({ items, values, targetValue: "v2", selection })); |
| 97 | + expect(selector.currentId).toBe(optionB.id); |
| 98 | + }); |
| 99 | + |
| 100 | + it("falls back to loadSelectedValue when new value is not in loaded options", () => { |
| 101 | + const selector = new DatabaseSingleSelectionSelector({ filterInputDebounceInterval: 200 }); |
| 102 | + const selection = new SelectionSingleValueBuilder().build(); |
| 103 | + const soleItems = [optionA]; |
| 104 | + const soleValues = new Map<string, string>([[optionA.id, "v1"]]); |
| 105 | + |
| 106 | + selector.updateProps(buildProps({ items: soleItems, values: soleValues, targetValue: "v1", selection })); |
| 107 | + const loadSpy = jest.spyOn(selector.options, "loadSelectedValue"); |
| 108 | + |
| 109 | + selector.updateProps(buildProps({ items: soleItems, values: soleValues, targetValue: "v-unknown", selection })); |
| 110 | + |
| 111 | + expect(loadSpy).toHaveBeenCalledWith("v-unknown", expect.anything()); |
| 112 | + }); |
| 113 | + |
| 114 | + it("clears currentId and selection when targetAttribute.value is cleared externally", () => { |
| 115 | + const selector = new DatabaseSingleSelectionSelector({ filterInputDebounceInterval: 200 }); |
| 116 | + const selection = new SelectionSingleValueBuilder().build(); |
| 117 | + const setSelectionSpy = jest.spyOn(selection, "setSelection"); |
| 118 | + |
| 119 | + selector.updateProps(buildProps({ items, values, targetValue: "v1", selection })); |
| 120 | + expect(selector.currentId).toBe(optionA.id); |
| 121 | + |
| 122 | + selector.updateProps(buildProps({ items, values, targetValue: undefined, selection })); |
| 123 | + |
| 124 | + expect(selector.currentId).toBeNull(); |
| 125 | + expect(setSelectionSpy).toHaveBeenCalledWith(undefined); |
| 126 | + }); |
| 127 | + |
| 128 | + it("does not re-resolve currentId when targetAttribute.value is unchanged", () => { |
| 129 | + const selector = new DatabaseSingleSelectionSelector({ filterInputDebounceInterval: 200 }); |
| 130 | + const selection = new SelectionSingleValueBuilder().build(); |
| 131 | + |
| 132 | + selector.updateProps(buildProps({ items, values, targetValue: "v1", selection })); |
| 133 | + const getAllSpy = jest.spyOn(selector.options, "getAll"); |
| 134 | + |
| 135 | + selector.updateProps(buildProps({ items, values, targetValue: "v1", selection })); |
| 136 | + |
| 137 | + expect(getAllSpy).not.toHaveBeenCalled(); |
| 138 | + expect(selector.currentId).toBe(optionA.id); |
| 139 | + }); |
| 140 | +}); |
0 commit comments