@ryupold/vode 1.8.3 → 1.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ import { expect } from "./helper";
2
+ import { memo, DIV, app, createState, SPAN } from "../index";
3
+
4
+ export default {
5
+ "memo(): returns the given function": () => {
6
+ const fn = (s: any) => [DIV];
7
+ const result = memo([1, 2], fn);
8
+ expect(result === fn).toEqual(true);
9
+ },
10
+
11
+ "memo(): throws when compare is not an array": () => {
12
+ const err = expect(() => memo(null as any, (s: any) => [DIV]))
13
+ .toFail();
14
+ expect(err.message)
15
+ .toEqual("first argument to memo() must be an array of values to compare");
16
+ },
17
+
18
+ "memo(): throws when componentOrProps is not a function": () => {
19
+ const err = expect(() => memo([1], null as any))
20
+ .toFail();
21
+ expect(err.message)
22
+ .toEqual("second argument to memo() must be a function that returns a vode or props object");
23
+ },
24
+
25
+ "memo(): integration with app prevents re-render when deps match": () => {
26
+ const state = createState({ count: 12 });
27
+ const root = document.createElement("div");
28
+ const container = document.createElement("div");
29
+ root.appendChild(container);
30
+
31
+ let callCount = 0;
32
+ app<typeof state>(container, state, (s) => [DIV, memo(
33
+ [s.count],
34
+ (s) => {
35
+ callCount++;
36
+ return [DIV, [SPAN, `${s.count}`]];
37
+ }
38
+ )]);
39
+
40
+
41
+ expect(callCount).toEqual(1);
42
+
43
+ state.patch({ count: 12 }); //same value, should not re-render
44
+ expect(callCount).toEqual(1);
45
+ state.patch({ count: 13 }); //different value, should re-render
46
+ expect(callCount).toEqual(2);
47
+ expect(container).toMatch(
48
+ [DIV,
49
+ [DIV,
50
+ [SPAN, "13"]
51
+ ]
52
+ ]
53
+ );
54
+ },
55
+
56
+ "memo(): works also with props factory": () => {
57
+ const state = createState({ count: 12, prefix: "Count is: " });
58
+ const root = document.createElement("div");
59
+ const container = document.createElement("div");
60
+ root.appendChild(container);
61
+
62
+ let callCount = 0;
63
+ app<typeof state>(container, state, (s) => [DIV,
64
+ [DIV,
65
+ memo(
66
+ [s.count],
67
+ (s) => {
68
+ callCount++;
69
+ return {
70
+ class: {
71
+ low: s.count < 10,
72
+ high: s.count >= 10,
73
+ }
74
+ };
75
+ }
76
+ ),
77
+ [SPAN, `${s.prefix}${s.count}`]
78
+ ],
79
+ ]);
80
+
81
+
82
+ expect(callCount).toEqual(1);
83
+ state.patch({ count: 12 });
84
+ expect(callCount).toEqual(1); // unchanged count should not cause re-render
85
+ state.patch({ count: 13 });
86
+ expect(callCount).toEqual(2);
87
+ state.patch({ prefix: "count: " });
88
+ expect(callCount).toEqual(3);
89
+ expect(container).toMatch(
90
+ [DIV,
91
+ [DIV, { class: { low: false, high: true } },
92
+ [SPAN, "count: 13"]
93
+ ]
94
+ ]
95
+ );
96
+ },
97
+
98
+ "memo(): can be a nested component function": () => {
99
+ const state = createState({ count: 12 });
100
+ const root = document.createElement("div");
101
+ const container = document.createElement("div");
102
+ root.appendChild(container);
103
+
104
+ let callCount = 0;
105
+ app<typeof state>(container, state, (s) => [DIV,
106
+ () => memo(
107
+ [s.count],
108
+ (s) => {
109
+ callCount++;
110
+ return [DIV, [SPAN, `${s.count}`]];
111
+ }
112
+ )]);
113
+
114
+
115
+ expect(callCount).toEqual(1);
116
+ state.patch({ count: 12 }); //same value, should not re-render
117
+ expect(callCount).toEqual(1);
118
+ },
119
+ };
@@ -0,0 +1,63 @@
1
+ import { mergeClass } from "../index";
2
+ import { expect } from "./helper";
3
+
4
+ export default {
5
+ "mergeClass(): no args returns null": () => {
6
+ expect(mergeClass()).toEqual(null);
7
+ },
8
+
9
+ "mergeClass(): single string returns it": () => {
10
+ expect(mergeClass("foo")).toEqual("foo");
11
+ },
12
+
13
+ "mergeClass(): two strings are joined and deduplicated": () => {
14
+ expect(mergeClass("foo", "bar")).toEqual("foo bar");
15
+ expect(mergeClass("foo bar", "bar baz")).toEqual("foo bar baz");
16
+ },
17
+
18
+ "mergeClass(): string and array": () => {
19
+ expect(mergeClass("foo", ["bar", "baz"])).toEqual("bar baz foo");
20
+ },
21
+
22
+ "mergeClass(): array and string": () => {
23
+ expect(mergeClass(["foo", "bar"], "baz")).toEqual("foo bar baz");
24
+ },
25
+
26
+ "mergeClass(): two arrays": () => {
27
+ expect(mergeClass(["foo", "bar"], ["baz", "qux"])).toEqual("foo bar baz qux");
28
+ },
29
+
30
+ "mergeClass(): two string arrays with duplicates": () => {
31
+ expect(mergeClass(["foo", "bar"], ["bar", "baz"])).toEqual("foo bar baz");
32
+ },
33
+
34
+ "mergeClass(): string and object": () => {
35
+ expect(mergeClass("foo", { bar: true, baz: false })).toEqual({ foo: true, bar: true, baz: false });
36
+ },
37
+
38
+ "mergeClass(): object and string": () => {
39
+ expect(mergeClass({ foo: true, bar: false }, "baz")).toEqual({ foo: true, bar: false, baz: true });
40
+ },
41
+
42
+ "mergeClass(): two objects": () => {
43
+ expect(mergeClass({ foo: true, bar: true }, { bar: false, baz: true })).toEqual({ foo: true, bar: false, baz: true });
44
+ },
45
+
46
+ "mergeClass(): object and array": () => {
47
+ expect(mergeClass({ foo: true }, ["bar", "baz"])).toEqual({ foo: true, 0: "bar", 1: "baz" });
48
+ },
49
+
50
+ "mergeClass(): array and object": () => {
51
+ expect(mergeClass(["foo", "bar"], { baz: true, qux: false })).toEqual({ 0: "foo", 1: "bar", baz: true, qux: false });
52
+ },
53
+
54
+ "mergeClass(): falsy entries are skipped": () => {
55
+ expect(mergeClass("foo", null, "bar")).toEqual("foo bar");
56
+ expect(mergeClass(null, "foo", undefined, "bar")).toEqual("foo bar");
57
+ },
58
+
59
+ "mergeClass(): multiple args (3+)": () => {
60
+ expect(mergeClass("a", "b", "c")).toEqual("a b c");
61
+ expect(mergeClass("x", null, ["y", "z"], "w")).toEqual("y z x w");
62
+ }
63
+ };
@@ -0,0 +1,43 @@
1
+ import { mergeProps, Props } from "../index";
2
+ import { expect } from "./helper";
3
+
4
+ export default {
5
+ "mergeProps(): no args returns undefined": () => {
6
+ expect(mergeProps()).toEqual(undefined);
7
+ },
8
+
9
+ "mergeProps(): single arg returned as-is": () => {
10
+ const p = { class: "foo" };
11
+ expect(mergeProps(p) === p).toEqual(true);
12
+ },
13
+
14
+ "mergeProps(): two plain objects merged": () => {
15
+ expect(mergeProps({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
16
+ },
17
+
18
+ "mergeProps(): right overwrites left for simple keys": () => {
19
+ expect(mergeProps({ a: 1, b: "x" }, { b: 2 })).toEqual({ a: 1, b: 2 });
20
+ },
21
+
22
+ "mergeProps(): class merged via mergeClass": () => {
23
+ expect(mergeProps({ class: "foo" }, { class: "bar" })).toEqual({ class: "foo bar" });
24
+ },
25
+
26
+ "mergeProps(): style merged via mergeStyle (strings)": () => {
27
+ const result = mergeProps({ style: "color: red" }, { style: "font-size: 14px" }) as Props;
28
+ expect((<string>result.style!).includes("color: red")).toEqual(true);
29
+ expect((<string>result.style!).includes("font-size: 14px")).toEqual(true);
30
+ },
31
+
32
+ "mergeProps(): null and undefined entries skipped": () => {
33
+ expect(mergeProps({ a: 1 }, null, { b: 2 }, undefined)).toEqual({ a: 1, b: 2 });
34
+ },
35
+
36
+ "mergeProps(): multiple args (3+)": () => {
37
+ expect(mergeProps({ a: 1 }, { b: 2 }, { c: 3 })).toEqual({ a: 1, b: 2, c: 3 });
38
+ },
39
+
40
+ "mergeProps(): first arg null returns defined from later args": () => {
41
+ expect(mergeProps(null, { a: 1 })).toEqual({ a: 1 });
42
+ }
43
+ };
@@ -0,0 +1,39 @@
1
+ import { mergeStyle } from "../index";
2
+ import { expect } from "./helper";
3
+
4
+ export default {
5
+ "mergeStyle(): no args returns empty string": () => {
6
+ expect(mergeStyle()).toEqual("");
7
+ },
8
+
9
+ "mergeStyle(): object style sets properties, returns cssText": () => {
10
+ const result = mergeStyle({ color: "red", fontSize: "14px" });
11
+ expect(typeof result).toEqual("string");
12
+ },
13
+
14
+ "mergeStyle(): single string starts with semicolon": () => {
15
+ expect(mergeStyle("color: red")).toEqual(";color: red");
16
+ },
17
+
18
+ "mergeStyle(): two strings are concatenated": () => {
19
+ expect(mergeStyle("color: red", "font-size: 14px")).toEqual(";color: red;font-size: 14px");
20
+ },
21
+
22
+ "mergeStyle(): object then string": () => {
23
+ const result = mergeStyle({ color: "red" }, "font-size: 14px") as string;
24
+ expect(result.indexOf("font-size: 14px") > 0).toEqual(true);
25
+ },
26
+
27
+ "mergeStyle(): null and undefined entries are skipped": () => {
28
+ expect(mergeStyle(null, "color: red", undefined)).toEqual(";color: red");
29
+ },
30
+
31
+ "mergeStyle(): multiple objects and strings alternate": () => {
32
+ const result = mergeStyle(
33
+ { color: "red" },
34
+ "font-size: 14px",
35
+ { background: "blue" }
36
+ ) as string;
37
+ expect(result.includes("font-size: 14px")).toEqual(true);
38
+ }
39
+ };
@@ -0,0 +1,34 @@
1
+ import { props, DIV, SPAN } from "../index";
2
+ import { expect } from "./helper";
3
+
4
+ export default {
5
+ "props(): on vode with props returns props object": () => {
6
+ expect(props([DIV, { class: "foo" }, "hello"]))
7
+ .toEqual({ class: "foo" });
8
+ },
9
+
10
+ "props(): on just-tag vode returns undefined": () => {
11
+ expect(props([DIV])).toEqual(undefined);
12
+ },
13
+
14
+ "props(): on text vode returns undefined": () => {
15
+ expect(props("hello")).toEqual(undefined);
16
+ },
17
+
18
+ "props(): on vode where second element is an array (child) returns undefined": () => {
19
+ expect(props([DIV, [SPAN]] as any)).toEqual(undefined);
20
+ },
21
+
22
+ "props(): on vode where second element is null returns undefined": () => {
23
+ expect(props([DIV, null as any, "hi"])).toEqual(undefined);
24
+ },
25
+
26
+ "props(): on falsy input returns undefined": () => {
27
+ expect(props(null as any)).toEqual(undefined);
28
+ expect(props(undefined as any)).toEqual(undefined);
29
+ },
30
+
31
+ "props(): on vode with length 1 returns undefined": () => {
32
+ expect(props([DIV] as any)).toEqual(undefined);
33
+ }
34
+ };
@@ -0,0 +1,106 @@
1
+ import { expect } from "./helper";
2
+ import { context } from "../src/state-context";
3
+ import { createState } from "../src/vode";
4
+
5
+ export default {
6
+ "StateContext.state: returns the state reference": () => {
7
+ const state = createState({ x: 10 });
8
+ const ctx = context(state);
9
+
10
+ expect((ctx).state === state)
11
+ .toEqual(true);
12
+ },
13
+
14
+ "StateContext.get() returns whole state": () => {
15
+ const state = createState({ a: 1, b: 2 });
16
+ const ctx = context(state);
17
+
18
+ expect(ctx.get())
19
+ .toEqual({ a: 1, b: 2 });
20
+ },
21
+
22
+
23
+ "StateContext.get(): deep nested": () => {
24
+ const state = createState({ a: { b: { c: 42 } } });
25
+ const ctx = context(state);
26
+
27
+ expect(ctx.a.b.c.get())
28
+ .toEqual(42);
29
+ },
30
+
31
+ "StateContext.get(): missing nested path returns undefined": () => {
32
+ const state = createState({ a: {} });
33
+ const ctx = context(state);
34
+
35
+ expect((ctx.a as any).b.get())
36
+ .toEqual(undefined);
37
+ },
38
+
39
+ "StateContext.put(): silently mutates state": () => {
40
+ const state = createState({ a: { b: 1 } });
41
+ const ctx = context(state);
42
+ ctx.a.b.put(2);
43
+
44
+ expect(state.a.b)
45
+ .toEqual(2);
46
+ },
47
+
48
+ "StateContext.put() on nested object replaces the sub-object": () => {
49
+ const state = createState({ a: { b: { x: 1, y: 2 } } });
50
+ const ctx = context(state);
51
+ ctx.a.b.put({ y: 99 });
52
+
53
+ expect(state.a.b)
54
+ .toEqual({ y: 99 });
55
+ },
56
+
57
+ "StateContext.put() at root level with empty keys": () => {
58
+ const state = createState({ a: 1, b: 2 });
59
+ const ctx = context(state);
60
+ ctx.put({ b: undefined });
61
+
62
+ expect(state)
63
+ .toEqual({ a: 1 });
64
+ },
65
+
66
+ "StateContext.patch(): calls state.patch with proper deep partial": () => {
67
+ const state = createState({ a: { b: 1 } });
68
+ const ctx = context(state);
69
+ ctx.a.b.patch(2);
70
+
71
+ const patches = (state as any).patch.initialPatches;
72
+
73
+ expect(patches.length)
74
+ .toEqual(1);
75
+ expect(patches[0])
76
+ .toEqual({ a: { b: 2 } });
77
+ },
78
+
79
+ "StateContext.patch(): async wraps in array": () => {
80
+ const state = createState({ a: { b: 1 } });
81
+ const ctx = context(state);
82
+ ctx.a.b.patch(2, true);
83
+
84
+ const patches = (state as any).patch.initialPatches;
85
+
86
+ expect(patches.length)
87
+ .toEqual(1);
88
+ expect(Array.isArray(patches[0]))
89
+ .toEqual(true);
90
+ expect(patches[0][0])
91
+ .toEqual({ a: { b: 2 } });
92
+ },
93
+
94
+ "StateContext.patch() on nested deep path three levels": () => {
95
+ const state = createState({ x: { y: { z: 0 } } });
96
+ const ctx = context(state);
97
+ ctx.x.y.z.patch(100);
98
+
99
+ const patches = (state as any).patch.initialPatches;
100
+
101
+ expect(patches.length)
102
+ .toEqual(1);
103
+ expect(patches[0])
104
+ .toEqual({ x: { y: { z: 100 } } });
105
+ },
106
+ };
@@ -0,0 +1,33 @@
1
+ import { tag, DIV, SPAN } from "../index";
2
+ import { expect } from "./helper";
3
+
4
+ export default {
5
+ "tag(): on a vode returns the tag name": () => {
6
+ expect(tag([DIV]))
7
+ .toEqual("div");
8
+ },
9
+
10
+ "tag(): on a vode with props and children": () => {
11
+ expect(tag([DIV, { class: "foo" }, [SPAN, "hi"]]))
12
+ .toEqual("div");
13
+ },
14
+
15
+ "tag(): on a text vode (string) returns #text": () => {
16
+ expect(tag("hello"))
17
+ .toEqual("#text");
18
+ },
19
+
20
+ "tag(): on falsy values returns undefined": () => {
21
+ expect(tag(null)).toEqual(undefined);
22
+ expect(tag(undefined)).toEqual(undefined);
23
+ },
24
+
25
+ "tag(): on no-vode values returns undefined": () => {
26
+ expect(tag(0)).toEqual(undefined);
27
+ expect(tag(true)).toEqual(undefined);
28
+ },
29
+
30
+ "tag(): on empty array returns undefined": () => {
31
+ expect(tag([])).toEqual(undefined);
32
+ }
33
+ };
@@ -0,0 +1,27 @@
1
+ import { vode, DIV, Vode, SPAN, STRONG } from "../index";
2
+ import { expect } from "./helper";
3
+
4
+ export default {
5
+ "vode(): passing an already constructed vode returns it": () => {
6
+ const testVode: Vode = [DIV, { class: 'test' }, "hello world"];
7
+
8
+ expect(vode(testVode)).toEqual(testVode);
9
+ },
10
+
11
+ "vode(): constructing a vode from parts": () => {
12
+ expect(
13
+ vode(DIV, { class: 'test' },
14
+ [SPAN, "hello"],
15
+ [STRONG, { style: 'color: green' }, "world"])
16
+ ).toEqual(
17
+ [DIV, { class: 'test' },
18
+ [SPAN, "hello"],
19
+ [STRONG, { style: 'color: green' }, "world"]]
20
+ );
21
+ },
22
+
23
+ "vode(): passing an invalid tag fails": () => {
24
+ const err = expect(() => vode(null as any)).toFail();
25
+ expect(err.message).toEqual("first argument to vode() must be a tag name or a vode");
26
+ }
27
+ };
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "test/",
5
+ "declaration": false,
6
+ "declarationMap": false,
7
+ "sourceMap": false,
8
+ "declarationDir": null,
9
+ "composite": false,
10
+ "noEmit": true
11
+
12
+ },
13
+ "include": [
14
+ "index.ts",
15
+ "src/**/*",
16
+ "test/**/*"
17
+ ]
18
+ }