|
| 1 | +// A descriptor that says `view: "dropdown"` must carry what a dropdown needs. |
| 2 | +// |
| 3 | +// 0.25.0 moved a `field.enum`'s FORM view from "text" to "dropdown". The descriptor kept |
| 4 | +// emitting no member list, so the generated `<Entity>` const told a consumer to render a |
| 5 | +// `<select>` and handed it nothing to fill one with — while the metadata had `@values` all |
| 6 | +// along. Every consumer then restated the members by hand; on the public reference app the |
| 7 | +// same four symbols appeared as literals across five hand-written files. |
| 8 | +// |
| 9 | +// Nothing caught it: the whole codegen suite passed unchanged when `options` was added, |
| 10 | +// because not one fixture asserted an enum field's descriptor output at all. This is that |
| 11 | +// assertion. |
| 12 | +import { describe, test, expect } from "bun:test"; |
| 13 | +import type { MetaObject } from "@metaobjectsdev/metadata"; |
| 14 | +import { MetaDataLoader, InMemoryStringSource } from "@metaobjectsdev/metadata"; |
| 15 | +import { renderEntityConstants } from "../src/templates/entity-constants.js"; |
| 16 | +import { buildUiFieldDescriptor } from "../src/templates/entity-ui-descriptor.js"; |
| 17 | + |
| 18 | +const MODEL = { |
| 19 | + "metadata.root": { |
| 20 | + package: "acme", |
| 21 | + children: [ |
| 22 | + // An abstract enum a concrete field inherits its members from, so the resolving |
| 23 | + // read (ADR-0039) is exercised rather than an own-only one. |
| 24 | + { "field.enum": { name: "orderState", abstract: true, "@values": ["open", "closed"] } }, |
| 25 | + { |
| 26 | + "object.entity": { |
| 27 | + name: "Order", |
| 28 | + children: [ |
| 29 | + { "source.rdb": { "@kind": "table", "@table": "orders" } }, |
| 30 | + { "field.long": { name: "id", "@required": true } }, |
| 31 | + { "identity.primary": { name: "pk", "@fields": ["id"], "@generation": "increment" } }, |
| 32 | + { "field.enum": { name: "status", "@values": ["pending", "shipped", "cancelled"] } }, |
| 33 | + { "field.enum": { name: "state", extends: "orderState" } }, |
| 34 | + { "field.string": { name: "note" } }, |
| 35 | + ], |
| 36 | + }, |
| 37 | + }, |
| 38 | + ], |
| 39 | + }, |
| 40 | +}; |
| 41 | + |
| 42 | +async function loadOrder(): Promise<MetaObject> { |
| 43 | + const result = await new MetaDataLoader().load([new InMemoryStringSource(JSON.stringify(MODEL))]); |
| 44 | + if (result.errors.length > 0) throw new Error(result.errors.map((e) => e.message).join("\n")); |
| 45 | + return result.root.objects().find((o) => o.name === "Order")!; |
| 46 | +} |
| 47 | + |
| 48 | +describe("an enum field's descriptor carries its members", () => { |
| 49 | + test("a declared enum reports view dropdown AND its options", async () => { |
| 50 | + const order = await loadOrder(); |
| 51 | + const status = order.fields().find((f) => f.name === "status")!; |
| 52 | + const d = buildUiFieldDescriptor(status); |
| 53 | + expect(d.view).toBe("dropdown"); |
| 54 | + expect(d.options).toEqual(["pending", "shipped", "cancelled"]); |
| 55 | + }); |
| 56 | + |
| 57 | + test("members inherited through extends resolve too", async () => { |
| 58 | + const order = await loadOrder(); |
| 59 | + const state = order.fields().find((f) => f.name === "state")!; |
| 60 | + // An own-only read would return undefined here and the dropdown would be empty. |
| 61 | + expect(buildUiFieldDescriptor(state).options).toEqual(["open", "closed"]); |
| 62 | + }); |
| 63 | + |
| 64 | + test("a non-enum field carries no options key", async () => { |
| 65 | + const order = await loadOrder(); |
| 66 | + const note = order.fields().find((f) => f.name === "note")!; |
| 67 | + expect(buildUiFieldDescriptor(note).options).toBeUndefined(); |
| 68 | + }); |
| 69 | + |
| 70 | + test("the emitted const spells the members, and only for the enums", async () => { |
| 71 | + const out = renderEntityConstants(await loadOrder()).toString(); |
| 72 | + expect(out).toContain(`options: ["pending", "shipped", "cancelled"] as const`); |
| 73 | + expect(out).toContain(`options: ["open", "closed"] as const`); |
| 74 | + // Exactly two option lists — the string field must not grow one. |
| 75 | + expect(out.match(/options:/g)?.length).toBe(2); |
| 76 | + }); |
| 77 | +}); |
0 commit comments