@excom/neutron 0.1.0

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.
Files changed (63) hide show
  1. package/.rush/temp/chunked-rush-logs/neutron.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/neutron.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +6 -0
  11. package/index.ts +9 -0
  12. package/package.json +45 -0
  13. package/rush-logs/neutron.apply-exports.cache.log +1 -0
  14. package/rush-logs/neutron.apply-exports.log +1 -0
  15. package/rush-logs/neutron.build_package-metas.cache.log +1 -0
  16. package/rush-logs/neutron.build_package-metas.log +1 -0
  17. package/src/command.ts +102 -0
  18. package/src/common-element.ts +377 -0
  19. package/src/constants.ts +101 -0
  20. package/src/devtools-hook.ts +93 -0
  21. package/src/lifecycle-configs.ts +304 -0
  22. package/src/neutron-element.ts +72 -0
  23. package/src/neutron-error.ts +6 -0
  24. package/src/neutron-internal.ts +550 -0
  25. package/src/neutron.ts +36 -0
  26. package/src/types/effect.types.ts +104 -0
  27. package/src/types/element.types.ts +263 -0
  28. package/src/types/index.ts +4 -0
  29. package/src/types/new.types.ts +159 -0
  30. package/src/types/shared.types.ts +25 -0
  31. package/src/utils/effect.ts +357 -0
  32. package/src/utils/element.ts +382 -0
  33. package/src/utils/index.ts +2 -0
  34. package/support/docs/COMMANDS.md +58 -0
  35. package/support/docs/COMPOSE.md +32 -0
  36. package/support/docs/DEBUG.md +11 -0
  37. package/support/docs/DEFINE.md +20 -0
  38. package/support/docs/EFFECTS.md +49 -0
  39. package/support/docs/EVENTS.md +57 -0
  40. package/support/docs/LIFECYCLES.md +69 -0
  41. package/support/docs/METHODS.md +49 -0
  42. package/support/docs/PROMISE_PROPS.md +29 -0
  43. package/support/docs/PROPS.md +64 -0
  44. package/support/docs/PROP_REACTIONS.md +35 -0
  45. package/support/docs/PROVISION.md +31 -0
  46. package/support/docs/README.md +118 -0
  47. package/support/docs/RECOMPOSE.md +70 -0
  48. package/support/docs/TYPESCRIPT.md +51 -0
  49. package/support/docs-sections.json +42 -0
  50. package/support/package-meta.json +129 -0
  51. package/support/tests/commands.test.ts +330 -0
  52. package/support/tests/common-element.test.ts +342 -0
  53. package/support/tests/devtools-hook.test.ts +209 -0
  54. package/support/tests/devtools-renderer.test.ts +125 -0
  55. package/support/tests/effects.test.ts +253 -0
  56. package/support/tests/element-config.test.ts +331 -0
  57. package/support/tests/entry.test.ts +68 -0
  58. package/support/tests/lifecycles.test.ts +489 -0
  59. package/support/tests/loop-guard.test.ts +162 -0
  60. package/support/tests/neutron.test.ts +1286 -0
  61. package/support/tests/recompose.test.ts +129 -0
  62. package/support/tests/utils.test.ts +75 -0
  63. package/tsconfig.json +5 -0
@@ -0,0 +1,253 @@
1
+ import { Neutron } from "../../src/neutron";
2
+ import { NeutronError } from "../../src/neutron-error";
3
+ import { effector, processEffectorResult } from "../../src/utils/effect";
4
+ import {
5
+ afterEach,
6
+ describe,
7
+ expect,
8
+ fixture,
9
+ it,
10
+ vi,
11
+ wait,
12
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
13
+ import { KitLogger } from "@excom/kit-logger";
14
+ import { observeProperty } from "@excom/kit-utils";
15
+
16
+ Neutron({
17
+ tag: "order-host",
18
+ props: {
19
+ childEl: { type: HTMLElement, store: "weak" },
20
+ stepText: String,
21
+ provision: Object,
22
+ recordFn: Function,
23
+ payload: Object,
24
+ },
25
+ })
26
+ .defineMethods({
27
+ // applies whatever effect the caller passes
28
+ run: (_el, effect) => effect,
29
+ })
30
+ .define();
31
+
32
+ const mount = async () => {
33
+ const el = fixture<any>(`<order-host></order-host>`);
34
+ await wait(0);
35
+ return el;
36
+ };
37
+
38
+ describe("Effects: application", () => {
39
+ afterEach(() => {
40
+ document.body.innerHTML = "";
41
+ });
42
+
43
+ it("applies the keys of one effect in the documented order", async () => {
44
+ const el = await mount();
45
+ const order: string[] = [];
46
+ observeProperty(el, "childEl", () => {
47
+ order.push("childEl");
48
+ });
49
+ observeProperty(el, "stepText", () => {
50
+ order.push("stepText");
51
+ });
52
+ observeProperty(el, "provision", () => {
53
+ order.push("provision");
54
+ });
55
+ el.recordFn = function () {
56
+ order.push(`method:${this === el}`);
57
+ };
58
+ el.addEventListener("order-host-done", () => {
59
+ order.push("emit");
60
+ });
61
+ // remove runs before add, so a listener removed and re-added in the
62
+ // same effect ends up registered exactly once
63
+ const fnX = vi.fn();
64
+ el.run({ addListener: ["order-host-x", fnX] });
65
+
66
+ const child = document.createElement("span");
67
+ const result = el.run({
68
+ emit: ["order-host-done"],
69
+ recordFn: [],
70
+ provision: { ready: true },
71
+ stepText: "step",
72
+ childEl: child,
73
+ addListener: ["order-host-x", fnX],
74
+ removeListener: ["order-host-x", fnX],
75
+ returns: "done",
76
+ });
77
+ expect(result).toBe("done");
78
+ expect(order).toEqual([
79
+ "childEl",
80
+ "stepText",
81
+ "method:true",
82
+ "provision",
83
+ "emit",
84
+ ]);
85
+ expect(el.childEl).toBe(child);
86
+ expect(el.provision).toEqual({ ready: true });
87
+ el.dispatchEvent(new CustomEvent("order-host-x"));
88
+ expect(fnX).toHaveBeenCalledTimes(1);
89
+ });
90
+
91
+ it("collects `returns` across array effects and skips empty / non-object entries", async () => {
92
+ const el = await mount();
93
+ expect(
94
+ el.run([
95
+ { stepText: "a" },
96
+ null,
97
+ false,
98
+ { stepText: "b", returns: 1 },
99
+ { returns: 2 },
100
+ ])
101
+ ).toEqual([1, 2]);
102
+ expect(el.stepText).toBe("b");
103
+ expect(el.run({ stepText: "c" })).toBeUndefined();
104
+ expect(el.stepText).toBe("c");
105
+ // non-POJO results are ignored
106
+ expect(el.run(new Map())).toBeUndefined();
107
+ expect(el.run(undefined)).toBeUndefined();
108
+ });
109
+
110
+ it("calls native methods and Function props with array arguments; ignores empty values", async () => {
111
+ const el = await mount();
112
+ el.recordFn = vi.fn();
113
+ const onEmpty = vi.fn();
114
+ el.addEventListener("order-host-empty", onEmpty);
115
+ el.run({
116
+ setAttribute: ["data-marker", "set"],
117
+ focus: null,
118
+ blur: undefined,
119
+ recordFn: false,
120
+ emit: null,
121
+ emits: "",
122
+ });
123
+ expect(el.getAttribute("data-marker")).toBe("set");
124
+ expect(el.recordFn).not.toHaveBeenCalled();
125
+ expect(onEmpty).not.toHaveBeenCalled();
126
+ el.run({ recordFn: ["a", "b"] });
127
+ expect(el.recordFn).toHaveBeenCalledWith("a", "b");
128
+ // a function held by an Object prop is a value, so it is replaced, not called
129
+ el.payload = () => "fn";
130
+ el.run({ payload: 5 });
131
+ expect(el.payload).toBe(5);
132
+ });
133
+
134
+ it("rejects non-array arguments for calls", async () => {
135
+ const el = await mount();
136
+ expect(() => el.run({ emit: true })).toThrow(NeutronError);
137
+ expect(() => el.run({ emit: true })).toThrow(
138
+ "Cannot call function `emit` on order-host - arguments must be an array. Received: `true`..."
139
+ );
140
+ expect(() => el.run({ setAttribute: "data-marker" })).toThrow(
141
+ "Cannot call function `setAttribute` on order-host - arguments must be an array. Received: `data-marke`..."
142
+ );
143
+ const unnamed = { toString: () => "", constructor: { name: "" } };
144
+ expect(() => el.run({ setAttribute: unnamed })).toThrow(
145
+ "Received: unknown"
146
+ );
147
+ });
148
+
149
+ it("validates element-prop effects", async () => {
150
+ const el = await mount();
151
+ expect(() => el.run({ childEl: { title: "x" } })).toThrow(
152
+ "Cannot set properties of `childEl` on element. Element must be set as a property first."
153
+ );
154
+ expect(() => el.run({ childEl: "nope" })).toThrow(
155
+ "Cannot set property `childEl` on order-host - value must be an element or an object."
156
+ );
157
+ const child = document.createElement("span");
158
+ el.run({ childEl: child });
159
+ el.run({ childEl: { title: "nested" } });
160
+ expect(child.title).toBe("nested");
161
+ el.run({ childEl: null });
162
+ expect(el.childEl).toBe(null);
163
+ // renderRoot is always treated as an element prop
164
+ const root = document.createElement("div");
165
+ el.run({ renderRoot: root });
166
+ el.run({ renderRoot: { title: "root" } });
167
+ expect(el.renderRoot).toBe(root);
168
+ expect(root.title).toBe("root");
169
+ });
170
+ });
171
+
172
+ describe("Effects: effector()", () => {
173
+ afterEach(() => {
174
+ vi.restoreAllMocks();
175
+ });
176
+
177
+ it("returns the same wrapper when given an effector and warns", () => {
178
+ const warn = vi.spyOn(KitLogger, "warn").mockImplementation(() => {});
179
+ const wrapped = effector(() => ({ title: "t" }));
180
+ expect(effector(wrapped)).toBe(wrapped);
181
+ expect(warn).toHaveBeenCalledTimes(1);
182
+ expect(warn.mock.calls[0][0]).toContain("is already an effector");
183
+ });
184
+
185
+ it("returns several `returns` values as an array and undefined without any", () => {
186
+ const div = document.createElement("div");
187
+ expect(
188
+ effector(() => [{ returns: 1 }, { title: "x" }, { returns: 2 }]).call(div)
189
+ ).toEqual([1, 2]);
190
+ expect(effector(() => ({ title: "y" })).call(div)).toBeUndefined();
191
+ expect(div.title).toBe("y");
192
+ });
193
+
194
+ it("validateInput reshapes arguments and validateOutput can veto a result", () => {
195
+ const div = document.createElement("div");
196
+ const fn = vi.fn((_el, n: number) => ({ title: `n${n}`, returns: n }));
197
+ const run = effector(fn, {
198
+ validateInput: ([el, n]) => [el, n * 2],
199
+ validateOutput: (_args, result: any) => result.returns < 10,
200
+ });
201
+ expect(run.call(div, 2)).toBe(4);
202
+ expect(div.title).toBe("n4");
203
+ expect(run.call(div, 10)).toBeUndefined();
204
+ expect(div.title).toBe("n4");
205
+
206
+ const skipped = vi.fn(() => ({ title: "no" }));
207
+ expect(
208
+ effector(skipped, { validateInput: () => false }).call(div)
209
+ ).toBeUndefined();
210
+ expect(skipped).not.toHaveBeenCalled();
211
+ });
212
+
213
+ it("rethrows errors with or without a host element", () => {
214
+ const div = document.createElement("div");
215
+ expect(() =>
216
+ effector(() => {
217
+ throw new Error("unhosted");
218
+ })()
219
+ ).toThrow("unhosted");
220
+ expect(() =>
221
+ effector(() => {
222
+ throw new Error("hosted");
223
+ }).call(div)
224
+ ).toThrow("hosted");
225
+ });
226
+
227
+ it("delayNextTask defers the effect to the next task and returns a promise", async () => {
228
+ const div = document.createElement("div");
229
+ const p = effector(() => ({ title: "later", returns: "r" }), {
230
+ delayNextTask: true,
231
+ }).call(div);
232
+ expect(p).toBeInstanceOf(Promise);
233
+ expect(div.title).toBe("");
234
+ expect(await p).toBe("r");
235
+ expect(div.title).toBe("later");
236
+ });
237
+
238
+ it("processEffectorResult ignores non-object results and merges style", () => {
239
+ const div = document.createElement("div");
240
+ expect(processEffectorResult(div, null)).toBeUndefined();
241
+ expect(processEffectorResult(div, 0)).toBeUndefined();
242
+ expect(
243
+ processEffectorResult(div, {
244
+ returns: "r",
245
+ style: { color: "red" },
246
+ })
247
+ ).toBe("r");
248
+ expect(div.style.color).toBe("red");
249
+ processEffectorResult(div, { style: { marginTop: "1px" } });
250
+ expect(div.style.color).toBe("red");
251
+ expect(div.style.marginTop).toBe("1px");
252
+ });
253
+ });
@@ -0,0 +1,331 @@
1
+ import { Neutron } from "../../src/neutron";
2
+ import { NeutronElement } from "../../src/neutron-element";
3
+ import { NeutronError } from "../../src/neutron-error";
4
+ import {
5
+ createPropConfig,
6
+ initRenderRootConfig,
7
+ isBuiltInElement,
8
+ } from "../../src/utils/element";
9
+ import {
10
+ afterEach,
11
+ describe,
12
+ expect,
13
+ fixture,
14
+ it,
15
+ vi,
16
+ wait,
17
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
18
+ import { TokenList } from "@excom/kit-utils";
19
+
20
+ describe("Prop configuration", () => {
21
+ it("rejects incomplete configs, protected prop names, and protected attribute names", () => {
22
+ const build = (props: Record<string, unknown>) => () =>
23
+ Neutron({ tag: "bad-host", props: props as any });
24
+ expect(build({ badProp: {} })).toThrow(NeutronError);
25
+ expect(build({ badProp: {} })).toThrow(
26
+ 'Incorrect property config: "badProp"'
27
+ );
28
+ expect(build({ content: String })).toThrow(
29
+ 'Cannot use protected prop name: "content"'
30
+ );
31
+ expect(build({ renderRoot: HTMLElement })).toThrow(NeutronError);
32
+ expect(build({ qValue: String })).toThrow(
33
+ 'Cannot use protected attr: "q-value"'
34
+ );
35
+ expect(build({ onValue: String })).toThrow(
36
+ 'Cannot use protected attr: "on-value"'
37
+ );
38
+ // default props are allowed to use otherwise protected names
39
+ expect(
40
+ createPropConfig("isMounted", { type: Boolean, attr: false }, true).prop
41
+ ).toBe("isMounted");
42
+ });
43
+
44
+ it("expands a bare constructor and honours custom serializers", () => {
45
+ const bare = createPropConfig("labelText", String);
46
+ expect(bare).toMatchObject({
47
+ prop: "labelText",
48
+ attr: "label-text",
49
+ type: String,
50
+ store: "default",
51
+ notify: false,
52
+ isValid: null,
53
+ });
54
+ expect(bare.defaultValue()).toBe(null);
55
+
56
+ const custom = {
57
+ serialize: (v: unknown) => v,
58
+ deserialize: (v: unknown) => v,
59
+ };
60
+ expect(
61
+ createPropConfig("secretText", { type: String, store: custom as any })
62
+ ).toMatchObject({
63
+ serialize: custom.serialize,
64
+ deserialize: custom.deserialize,
65
+ store: custom,
66
+ });
67
+
68
+ const weak = createPropConfig("childEl", {
69
+ type: HTMLElement,
70
+ store: "weak",
71
+ });
72
+ expect(weak.attr).toBe(false);
73
+ const div = document.createElement("div");
74
+ expect(weak.serialize(div)).toBeInstanceOf(WeakRef);
75
+ expect(weak.deserialize(weak.serialize(div))).toBe(div);
76
+ // an unknown store name falls back to the default serializer
77
+ expect(
78
+ createPropConfig("childEl", { type: HTMLElement, store: "bogus" as any })
79
+ .serialize
80
+ ).toBe(bare.serialize);
81
+ });
82
+
83
+ it("initRenderRootConfig defaults the tag and derives defaultSlots from shadow", () => {
84
+ expect(initRenderRootConfig(undefined)).toBeUndefined();
85
+ expect(initRenderRootConfig({})).toEqual({
86
+ tag: "div",
87
+ shadow: undefined,
88
+ defaultSlots: false,
89
+ });
90
+ expect(initRenderRootConfig({ shadow: "open" })).toEqual({
91
+ tag: "div",
92
+ shadow: "open",
93
+ defaultSlots: true,
94
+ });
95
+ expect(
96
+ initRenderRootConfig({
97
+ tag: "main",
98
+ shadow: "closed",
99
+ defaultSlots: false,
100
+ })
101
+ ).toEqual({ tag: "main", shadow: "closed", defaultSlots: false });
102
+ expect(
103
+ initRenderRootConfig({ shadow: "bogus" as any, defaultSlots: true })
104
+ ).toEqual({ tag: "div", shadow: undefined, defaultSlots: true });
105
+ });
106
+ });
107
+
108
+ describe("Prop reflection", () => {
109
+ const ReflectHost = Neutron({
110
+ tag: "reflect-props",
111
+ props: {
112
+ labelText: String,
113
+ countValue: Number,
114
+ isOpen: Boolean,
115
+ tagNames: TokenList,
116
+ payload: Object,
117
+ childEl: { type: HTMLElement, store: "weak" },
118
+ hardEl: HTMLElement,
119
+ secretText: {
120
+ type: String,
121
+ store: {
122
+ serialize: (v: unknown) => (v == null ? v : `s:${v}`),
123
+ deserialize: (v: unknown) =>
124
+ typeof v === "string" ? v.replace(/^s:/, "") : v,
125
+ },
126
+ },
127
+ },
128
+ }).onPropChanged(["labelText", "countValue"], vi.fn());
129
+ ReflectHost.define();
130
+
131
+ it("reflects primitives in both directions", () => {
132
+ const el = document.createElement("reflect-props") as any;
133
+ // String (observed attribute: read back from the prop store)
134
+ el.labelText = 5;
135
+ expect(el.getAttribute("label-text")).toBe("5");
136
+ expect(el.labelText).toBe("5");
137
+ el.setAttribute("label-text", "from-attr");
138
+ expect(el.labelText).toBe("from-attr");
139
+ el.labelText = null;
140
+ expect(el.hasAttribute("label-text")).toBe(false);
141
+ expect(el.labelText).toBe(null);
142
+ // Number
143
+ el.countValue = 3;
144
+ expect(el.getAttribute("count-value")).toBe("3");
145
+ el.setAttribute("count-value", "4.5");
146
+ expect(el.countValue).toBe(4.5);
147
+ el.setAttribute("count-value", "not-a-number");
148
+ expect(el.countValue).toBe(null);
149
+ el.setAttribute("count-value", "");
150
+ expect(el.countValue).toBe(null);
151
+ el.countValue = 7;
152
+ el.countValue = "";
153
+ expect(el.hasAttribute("count-value")).toBe(false);
154
+ // Boolean (unobserved attribute: parsed from the attribute on read)
155
+ el.isOpen = true;
156
+ expect(el.getAttribute("is-open")).toBe("");
157
+ el.isOpen = false;
158
+ expect(el.hasAttribute("is-open")).toBe(false);
159
+ el.setAttribute("is-open", "anything");
160
+ expect(el.isOpen).toBe(true);
161
+ el.removeAttribute("is-open");
162
+ expect(el.isOpen).toBe(false);
163
+ // TokenList
164
+ el.tagNames = ["a", "", null, "b"];
165
+ expect(el.getAttribute("tag-names")).toBe("a b");
166
+ expect(el.tagNames).toEqual(["a", "b"]);
167
+ el.setAttribute("tag-names", " x y ");
168
+ expect(el.tagNames).toEqual(["x", "y"]);
169
+ el.tagNames = null;
170
+ expect(el.hasAttribute("tag-names")).toBe(false);
171
+ expect(el.tagNames).toBe(null);
172
+ // custom serializer wraps the attribute value only
173
+ el.secretText = "abc";
174
+ expect(el.getAttribute("secret-text")).toBe("s:abc");
175
+ expect(el.secretText).toBe("abc");
176
+ });
177
+
178
+ it("stores rich props on the instance, weakly when configured", () => {
179
+ const el = document.createElement("reflect-props") as any;
180
+ el.payload = { a: 1 };
181
+ expect(el.hasAttribute("payload")).toBe(false);
182
+ expect(el.payload).toEqual({ a: 1 });
183
+ expect(el._n_.propStore.payload).toEqual({ a: 1 });
184
+ const child = document.createElement("span");
185
+ el.childEl = child;
186
+ expect(el._n_.propStore.childEl).toBeInstanceOf(WeakRef);
187
+ expect(el.childEl).toBe(child);
188
+ el.hardEl = child;
189
+ expect(el._n_.propStore.hardEl).toBe(child);
190
+ el.childEl = null;
191
+ expect(el.childEl).toBe(null);
192
+ // only props with a reaction observe their attribute
193
+ expect(ReflectHost.CustomElement.observedAttributes).toEqual([
194
+ "label-text",
195
+ "count-value",
196
+ ]);
197
+ });
198
+ });
199
+
200
+ describe("renderRoot", () => {
201
+ Neutron({
202
+ tag: "root-light",
203
+ props: {},
204
+ renderRoot: { tag: "section" },
205
+ }).define();
206
+ Neutron({
207
+ tag: "root-shadow",
208
+ props: {},
209
+ renderRoot: { shadow: "open" },
210
+ }).define();
211
+ Neutron({
212
+ tag: "root-closed",
213
+ props: {},
214
+ renderRoot: { tag: "main", shadow: "closed", defaultSlots: false },
215
+ }).define();
216
+
217
+ afterEach(() => {
218
+ document.body.innerHTML = "";
219
+ });
220
+
221
+ it("appends a light-DOM render root once, on first connect", async () => {
222
+ const el = fixture<any>(`<root-light><p>kept</p></root-light>`);
223
+ await wait(0);
224
+ expect(el.shadowRoot).toBe(null);
225
+ expect(el.renderRoot.localName).toBe("section");
226
+ expect(el.lastElementChild).toBe(el.renderRoot);
227
+ expect(el.querySelector("p")!.textContent).toBe("kept");
228
+ expect(el.querySelector("slot")).toBe(null);
229
+ el.remove();
230
+ await wait(0);
231
+ document.body.append(el);
232
+ expect(el.querySelectorAll("section")).toHaveLength(1);
233
+ });
234
+
235
+ it("attaches an open shadow root with a display:contents host and an adopt slot", async () => {
236
+ const el = fixture<any>(`<root-shadow></root-shadow>`);
237
+ await wait(0);
238
+ const root = el.shadowRoot as ShadowRoot;
239
+ expect(root).not.toBe(null);
240
+ expect(el.renderRoot.parentNode).toBe(root);
241
+ expect(el.renderRoot.localName).toBe("div");
242
+ expect(el.renderRoot.getAttribute("style")).toContain("display: contents");
243
+ const slot = root.querySelector(
244
+ "slot[name=neutron-adopt]"
245
+ ) as HTMLSlotElement;
246
+ expect(slot).not.toBe(null);
247
+
248
+ // slotted templates are imported into the shadow root; other slotted
249
+ // nodes are ignored
250
+ el.innerHTML = `<template slot="neutron-adopt"><p class="adopted">hi</p></template><span slot="neutron-adopt">no</span>`;
251
+ slot.dispatchEvent(new Event("slotchange"));
252
+ expect(root.querySelectorAll(".adopted")).toHaveLength(1);
253
+ expect(root.querySelector("span")).toBe(null);
254
+ // a re-import replaces the previous content instead of duplicating it
255
+ slot.dispatchEvent(new Event("slotchange"));
256
+ expect(root.querySelectorAll(".adopted")).toHaveLength(1);
257
+ });
258
+
259
+ it("supports a closed shadow root without default slots", async () => {
260
+ const el = fixture<any>(`<root-closed></root-closed>`);
261
+ await wait(0);
262
+ expect(el.shadowRoot).toBe(null);
263
+ const root = el.renderRoot.parentNode as ShadowRoot;
264
+ expect(root).toBeInstanceOf(ShadowRoot);
265
+ expect(root.mode).toBe("closed");
266
+ expect(el.renderRoot.localName).toBe("main");
267
+ expect(root.querySelector("slot")).toBe(null);
268
+ });
269
+ });
270
+
271
+ describe("Element definition", () => {
272
+ afterEach(() => {
273
+ document.body.innerHTML = "";
274
+ });
275
+
276
+ it("Neutron() without props still defines a working element", async () => {
277
+ const Empty = Neutron({ tag: "empty-host" } as any);
278
+ Empty.define();
279
+ const el = fixture<any>(`<empty-host></empty-host>`);
280
+ await wait(0);
281
+ expect(el.isMounted).toBe(true);
282
+ expect(Object.keys(Empty.runtimeConfig.props)).toEqual([
283
+ "isMounted",
284
+ "isAdopted",
285
+ "wasMounted",
286
+ "isMoving",
287
+ "renderRoot",
288
+ ]);
289
+ expect(isBuiltInElement(el)).toBe(false);
290
+ expect(isBuiltInElement(undefined as any)).toBe(false);
291
+ });
292
+ });
293
+
294
+ describe("static introspection", () => {
295
+ it("getConfig() returns the runtime config after define() and undefined before", () => {
296
+ const Builder = Neutron({
297
+ tag: "config-probe",
298
+ props: { isOpen: Boolean, openStage: Number, _secret: String },
299
+ events: { toggled: { prefixWithTag: true } },
300
+ });
301
+ const Ctor = Builder.CustomElement as unknown as typeof NeutronElement;
302
+ expect(Ctor.getConfig()).toBeUndefined();
303
+ Builder.define();
304
+ const config = Ctor.getConfig()!;
305
+ expect(config.tag).toBe("config-probe");
306
+ expect(Object.keys(config.props)).toEqual(
307
+ expect.arrayContaining(["isOpen", "openStage", "_secret"])
308
+ );
309
+ expect(config.lifecycles.propSet).toBeDefined();
310
+ // reachable through the registry and an instance's constructor
311
+ const Registered = customElements.get("config-probe") as typeof NeutronElement;
312
+ expect(Registered.getConfig()).toBe(config);
313
+ const el = document.createElement("config-probe");
314
+ expect((el.constructor as typeof NeutronElement).getConfig()).toBe(config);
315
+ });
316
+
317
+ it("getPropConfig() looks a prop up by attr or prop name", () => {
318
+ const Ctor = customElements.get("config-probe") as typeof NeutronElement;
319
+ expect(Ctor.getPropConfig({ attr: "open-stage" })).toMatchObject({
320
+ prop: "openStage",
321
+ attr: "open-stage",
322
+ type: Number,
323
+ });
324
+ expect(Ctor.getPropConfig({ prop: "isOpen" })).toMatchObject({
325
+ attr: "is-open",
326
+ type: Boolean,
327
+ });
328
+ expect(Ctor.getPropConfig({ attr: "nope" })).toBeUndefined();
329
+ expect(Ctor.getPropConfig({ prop: "nope" })).toBeUndefined();
330
+ });
331
+ });
@@ -0,0 +1,68 @@
1
+ import * as barrel from "../../index";
2
+ import { NeutronError } from "../../src/neutron-error";
3
+ import {
4
+ afterEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
9
+ import { NUCLEUS_DEVTOOLS_HOOK_KEY } from "@excom/kit-devtools";
10
+
11
+ describe("Package entry", () => {
12
+ afterEach(() => {
13
+ delete (globalThis as Record<string, unknown>)[
14
+ NUCLEUS_DEVTOOLS_HOOK_KEY
15
+ ];
16
+ });
17
+
18
+ it("re-exports the public surface", () => {
19
+ expect(barrel.Neutron).toBeTypeOf("function");
20
+ expect(barrel.NeutronElement).toBeTypeOf("function");
21
+ expect(barrel.NeutronInternal).toBeTypeOf("function");
22
+ expect(barrel.TokenList).toBeTypeOf("function");
23
+ expect(barrel.attachDevtools).toBeTypeOf("function");
24
+ expect(barrel.effector).toBeTypeOf("function");
25
+ expect(barrel.compose).toBe(barrel.Neutron.compose);
26
+ expect(barrel.Neutron.DOM.TokenList).toBe(barrel.TokenList);
27
+ expect(barrel.Neutron.attachDevtools).toBe(barrel.attachDevtools);
28
+ });
29
+
30
+ it("NeutronError carries its class name", () => {
31
+ const err = new NeutronError("boom");
32
+ expect(err).toBeInstanceOf(Error);
33
+ expect(err).toBeInstanceOf(NeutronError);
34
+ expect(err.name).toBe("NeutronError");
35
+ expect(err.message).toBe("boom");
36
+ class SubError extends NeutronError {}
37
+ expect(new SubError("x").name).toBe("SubError");
38
+ });
39
+
40
+ it("publishes `neutron/defined` with the tag and prop / attr pairs when an element is defined", () => {
41
+ const published: Array<{ path: string[]; meta: Record<string, unknown> }> =
42
+ [];
43
+ barrel.attachDevtools({
44
+ version: 1,
45
+ publicize: (path, meta) => published.push({ path: [...path], meta }),
46
+ });
47
+ barrel.Neutron({
48
+ tag: "defined-host",
49
+ props: {
50
+ labelText: String,
51
+ hostEl: { type: HTMLElement, store: "weak" },
52
+ },
53
+ });
54
+ const defined = published.filter(
55
+ (p) => p.path.join("/") === "neutron/defined"
56
+ );
57
+ expect(defined).toHaveLength(1);
58
+ expect(defined[0].meta).toEqual({
59
+ tag: "defined-host",
60
+ props: [
61
+ { prop: "labelText", attr: "label-text" },
62
+ { prop: "hostEl", attr: false },
63
+ ],
64
+ });
65
+ // a definition, not an instance: nothing to deref
66
+ expect(defined[0].meta.weakElement).toBeUndefined();
67
+ });
68
+ });