diff --git a/.changeset/spicy-camels-compare.md b/.changeset/spicy-camels-compare.md new file mode 100644 index 000000000..7f21dd4a1 --- /dev/null +++ b/.changeset/spicy-camels-compare.md @@ -0,0 +1,5 @@ +--- +"solid-js": patch +--- + +test: add tests to `splitProps` and `mergeProps` diff --git a/packages/solid/test/component.spec.ts b/packages/solid/test/component.spec.ts index 82d2763d9..9ddea5be6 100644 --- a/packages/solid/test/component.spec.ts +++ b/packages/solid/test/component.spec.ts @@ -62,17 +62,135 @@ describe("mergeProps", () => { expect(props.b).toBe(null); expect(props.c).toBe("j"); }); + it("skips undefined values", () => { + let bValue: number | undefined + const a = { value: 1 } + const b = { get value() { return bValue } } + const c = { get value() { return undefined } } + const props = mergeProps(a, b, c) + expect(props.value).toBe(1) + bValue = 2; + expect(props.value).toBe(2) + }); + it("keeps references", () => { + const a = { value1: 1 } + const b = { value2: 2 } + const props = mergeProps(a, b) + a.value1 = b.value2 = 3 + expect(props.value1).toBe(3) + expect(props.value2).toBe(3) + }); + it("with getter keeps references", () => { + const a = { value1: 1 } + const b = { get value2() { return undefined } } + const props = mergeProps(a, b) + a.value1 = 3 + expect(props.value1).toBe(3) + }); + it("overrides enumerables", () => { + const a = Object.defineProperties({}, { + value1: { + enumerable: false, + value: 2 + } + }) + const props = mergeProps({}, a) + expect((props as any).value1).toBe(2) + expect(Object.getOwnPropertyDescriptor(props, 'value1')?.enumerable).toBeTruthy() + expect(Object.keys(props).join()).toBe("value1") + }); + it("does not write the target", () => { + const props = { value1: 1 } + mergeProps(props, { value2: 2, get value3() { return 3 } }) + expect(Object.keys(props).join("")).toBe('value1') + }); + it("always returns a new reference", () => { + const props = {} + const newProps = mergeProps(props) + expect(props === newProps).toBeFalsy() + }); + it("uses the source instances", () => { + const source1 = { get a() { return this } } + const source2 = { get b() { return this } } + const props = mergeProps(source1, source2) + expect(props.a === source1).toBeTruthy() + expect(props.b === source2).toBeTruthy() + }); + it("does not clone nested objects", () => { + const b = { value: 1 } + const props = mergeProps({ a: 1 }, { b }) + b.value = 2 + expect(props.b.value).toBe(2) + }); + it("always returns getters", () => { + const props = mergeProps({ a: 1 }, { b: 2 }) + const desc = Object.getOwnPropertyDescriptors(props) + expect(!!desc.a.get).toBeTruthy() + expect(!!desc.b.get).toBeTruthy() + }); + it("always returns unwritables", () => { + const props = mergeProps({ a: 1 }, { b: 2 }) + const desc = Object.getOwnPropertyDescriptors(props) + expect(!!desc.a.writable).toBeFalsy() + expect(!!desc.b.writable).toBeFalsy() + }); + it("ignores undefined values", () => { + const props = mergeProps({ a: 1 }, { a: undefined }) + expect(props.a).toBe(1) + }); + it("handles null values", () => { + const props = mergeProps({ a: 1 }, { a: null }) + expect(props.a).toBeNull() + }); + it("contains null values", () => { + const props = mergeProps({ a: null, get b() { return null; } }) + expect(props.a).toBeNull(); + expect(props.b).toBeNull(); + }); + it("contains undefined values", () => { + const props = mergeProps({ a: undefined, get b() { return undefined; } }) + expect(Object.keys(props).join()).toBe('a,b') + expect('a' in props).toBeTruthy() + expect('b' in props).toBeTruthy() + expect(props.a).toBeUndefined(); + expect(props.b).toBeUndefined(); + }); + it("ignores falsy sources", () => { + const props = mergeProps(undefined, null, { value: 1 }, null, undefined) + expect(Object.keys(props).join()).toBe('value') + }); + it("fails with non objects sources", () => { + expect(() => mergeProps({ value: 1 }, true)).toThrowError() + expect(() => mergeProps({ value: 1 }, 1)).toThrowError() + }); + it("works with a array source", () => { + const props = mergeProps({ value: 1 }, [2]) + expect(Object.keys(props).join()).toBe('0,length,value') + expect(props.value).toBe(1) + expect(props.length).toBe(1) + expect(props[0]).toBe(2) + }); + it("is safe", () => { + mergeProps({}, JSON.parse('{ "__proto__": { "evil": true } }')) + expect(({} as any).evil).toBeUndefined() + mergeProps({}, JSON.parse('{ "prototype": { "evil": true } }')) + expect(({} as any).evil).toBeUndefined() + mergeProps({ value: 1 }, JSON.parse('{ "__proto__": { "evil": true } }')) + expect(({} as any).evil).toBeUndefined() + mergeProps({ value: 1 }, JSON.parse('{ "prototype": { "evil": true } }')) + expect(({} as any).evil).toBeUndefined() + }); }); describe("Set Default Props", () => { test("simple set", () => { let props: SimplePropTypes = { - get a() { - return "ji"; - }, - b: null, - c: "j" + get a() { + return "ji"; }, + b: null, + c: "j" + }, defaults: SimplePropTypes = { a: "yy", b: "ggg", d: "DD" }; props = mergeProps(defaults, props); expect(props.a).toBe("ji"); @@ -122,12 +240,12 @@ describe("Clone Store", () => { describe("Merge Signal", () => { test("simple set", () => { const [s, set] = createSignal({ - get a() { - return "ji"; - }, - b: null, - c: "j" - }), + get a() { + return "ji"; + }, + b: null, + c: "j" + }), defaults: SimplePropTypes = { a: "yy", b: "ggg", d: "DD" }; let props!: SimplePropTypes; const res: string[] = []; @@ -176,6 +294,83 @@ describe("SplitProps Props", () => { expect(out).toBe("Yo Bob"); }); }); + test("SplitProps result is inmutable", () => { + const inProps = { first: 1, second: 2 } + const [props, otherProps] = splitProps(inProps, ["first"]); + inProps.first = inProps.second = 3 + expect(props.first).toBe(1) + expect(otherProps.second).toBe(2) + }); + test("SplitProps clones the descriptor", () => { + let signalValue = 1 + const desc = { + signal: { + enumerable: true, + get() { + return signalValue + } + }, + static: { + configurable: true, + enumerable: false, + value: 2 + } + } + const inProps = Object.defineProperties({}, desc) as { signal: number, value1: number } + const [props, otherProps] = splitProps(inProps, ["signal"]); + + expect(props.signal).toBe(1) + signalValue++ + expect(props.signal).toBe(2) + + const signalDesc = Object.getOwnPropertyDescriptor(props, "signal")! + expect(signalDesc.get === desc.signal.get).toBeTruthy() + expect(signalDesc.set).toBeUndefined() + expect(signalDesc.enumerable).toBeTruthy() + expect(signalDesc.configurable).toBeFalsy() + + const staticDesc = Object.getOwnPropertyDescriptor(otherProps, "static")! + expect(staticDesc.value).toBe(2) + expect(staticDesc.get).toBeUndefined() + expect(staticDesc.set).toBeUndefined() + expect(staticDesc.enumerable).toBeFalsy() + expect(staticDesc.configurable).toBeTruthy() + + }); + test("SplitProps with multiple keys", () => { + const inProps: { + id?: string, + color?: string, + margin?: number, + padding?: number, + variant?: string, + description?: string + } = { + id: "input", + color: "red", + margin: 3, + variant: "outlined", + description: "test", + } + + const [styleProps, inputProps, otherProps] = splitProps( + inProps, + ["color", "margin", "padding"], + ["variant", "description"] + ); + + expect(styleProps.color).toBe("red") + expect(styleProps.margin).toBe(3) + expect(styleProps.padding).toBeUndefined() + expect(Object.keys(styleProps).length).toBe(2) + + expect(inputProps.description).toBe("test") + expect(inputProps.variant).toBe("outlined") + expect(Object.keys(inputProps).length).toBe(2) + + expect(otherProps.id).toBe("input") + expect(Object.keys(otherProps).length).toBe(1) + }); test("Merge SplitProps", () => { let value: string | undefined = undefined; const [splittedProps] = splitProps({ color: "blue" } as { color: string; other?: string }, [