@adobe/data-lit 0.9.83 → 0.9.86

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 (33) hide show
  1. package/dist/elements/database-element.d.ts +9 -0
  2. package/dist/elements/database-element.js +35 -1
  3. package/dist/elements/database-element.js.map +1 -1
  4. package/dist/elements/database-element.type-test.js +6 -0
  5. package/dist/elements/database-element.type-test.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/testing/template/from.d.ts +7 -0
  10. package/dist/testing/template/from.js +90 -0
  11. package/dist/testing/template/from.js.map +1 -0
  12. package/dist/testing/template/from.test.d.ts +1 -0
  13. package/dist/testing/template/from.test.js +41 -0
  14. package/dist/testing/template/from.test.js.map +1 -0
  15. package/dist/testing/template/public.d.ts +2 -0
  16. package/dist/testing/template/public.js +3 -0
  17. package/dist/testing/template/public.js.map +1 -0
  18. package/dist/testing/template/query.d.ts +25 -0
  19. package/dist/testing/template/query.js +3 -0
  20. package/dist/testing/template/query.js.map +1 -0
  21. package/dist/testing/template/template.d.ts +1 -0
  22. package/dist/testing/template/template.js +5 -0
  23. package/dist/testing/template/template.js.map +1 -0
  24. package/package.json +7 -3
  25. package/src/elements/database-element.ts +36 -1
  26. package/src/elements/database-element.type-test.ts +20 -0
  27. package/src/index.ts +1 -0
  28. package/src/testing/template/from.test.ts +47 -0
  29. package/src/testing/template/from.ts +86 -0
  30. package/src/testing/template/public.ts +3 -0
  31. package/src/testing/template/query.ts +27 -0
  32. package/src/testing/template/template.ts +4 -0
  33. package/tsconfig.tsbuildinfo +1 -1
@@ -12,6 +12,15 @@ export declare abstract class DatabaseElement<P extends Database.Plugin> extends
12
12
  */
13
13
  set service(db: Database.Plugin.ToDatabase<P>);
14
14
  get service(): UIService.FromService<Database.Plugin.ToDatabase<P>>;
15
+ /**
16
+ * The full, unrestricted database — for imperative-rendering subclasses
17
+ * (a canvas / WebGL draw loop that reads columns synchronously each frame,
18
+ * where the reactive `observe` surface can't help because systems mutate
19
+ * columns in place without firing observers). Presentations and external
20
+ * callers still receive only the restricted {@link service}; this privileged
21
+ * read path is confined to container element authors via `protected`.
22
+ */
23
+ protected get database(): Database.Plugin.ToDatabase<P>;
15
24
  constructor();
16
25
  abstract get plugin(): P;
17
26
  connectedCallback(): void;
@@ -5,7 +5,8 @@ import { Database } from '@adobe/data/ecs';
5
5
  import { UIService } from '@adobe/data/service';
6
6
  import { attachDecorator, withHooks } from '../index.js';
7
7
  export class DatabaseElement extends LitElement {
8
- /** Full database, hard-private — invisible to subclasses and external callers. */
8
+ /** Full database, hard-private. Exposed to subclasses only through the
9
+ * restricted {@link service} getter and the {@link database} accessor. */
9
10
  #database;
10
11
  /**
11
12
  * The element's database surface.
@@ -22,17 +23,50 @@ export class DatabaseElement extends LitElement {
22
23
  get service() {
23
24
  return UIService.restrict(this.#database);
24
25
  }
26
+ /**
27
+ * The full, unrestricted database — for imperative-rendering subclasses
28
+ * (a canvas / WebGL draw loop that reads columns synchronously each frame,
29
+ * where the reactive `observe` surface can't help because systems mutate
30
+ * columns in place without firing observers). Presentations and external
31
+ * callers still receive only the restricted {@link service}; this privileged
32
+ * read path is confined to container element authors via `protected`.
33
+ */
34
+ get database() {
35
+ return this.#database;
36
+ }
25
37
  constructor() {
26
38
  super();
27
39
  attachDecorator(this, 'render', withHooks);
28
40
  }
29
41
  connectedCallback() {
42
+ this.#upgradeServiceProperty();
30
43
  if (!this.#database) {
31
44
  const ancestor = this.findAncestorService();
32
45
  this.service = ancestor?.extend(this.plugin) ?? Database.create(this.plugin);
33
46
  }
34
47
  super.connectedCallback();
35
48
  }
49
+ /**
50
+ * A lazy wrapper binds `.service` *before* this element's class is upgraded
51
+ * (the element module is dynamically imported), so the value lands as an own
52
+ * data property that shadows the `service` accessor: `set service` never runs
53
+ * (`#database` stays unset, `database` reads `undefined`) and `get service`
54
+ * returns the raw injected database instead of the restricted view. Reassign
55
+ * the shadowed value through the accessor so the setter runs — `#database` is
56
+ * populated and the UI restriction is actually enforced.
57
+ */
58
+ #upgradeServiceProperty() {
59
+ if (!Object.prototype.hasOwnProperty.call(this, "service"))
60
+ return;
61
+ // The own property was bound by the wrapper as the full database (DI); the
62
+ // `service` accessor's declared type hides that, hence the widening view.
63
+ const self = this;
64
+ const injected = self.service;
65
+ delete self.service;
66
+ if (injected !== undefined) {
67
+ this.service = injected;
68
+ }
69
+ }
36
70
  findAncestorService() {
37
71
  for (const element of iterateSelfAndAncestors(this)) {
38
72
  // Read each ancestor's `service`. A DatabaseElement returns its full
@@ -1 +1 @@
1
- {"version":3,"file":"database-element.js","sourceRoot":"","sources":["../../src/elements/database-element.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,OAAgB,eAA2C,SAAQ,UAAU;IAEjF,kFAAkF;IAClF,SAAS,CAAiC;IAE1C;;;;;;OAMG;IACH,IAAI,OAAO,CAAC,EAAiC;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,aAAa,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,OAAO;QACT,OAAO,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5C,CAAC;IAED;QACE,KAAK,EAAE,CAAC;QACR,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC7C,CAAC;IAID,iBAAiB;QACf,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC5C,IAAI,CAAC,OAAO,GAAG,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/E,CAAC;QACD,KAAK,CAAC,iBAAiB,EAAE,CAAC;IAC5B,CAAC;IAES,mBAAmB;QAC3B,KAAK,MAAM,OAAO,IAAI,uBAAuB,CAAC,IAAI,CAAC,EAAE,CAAC;YACpD,qEAAqE;YACrE,uEAAuE;YACvE,wEAAwE;YACxE,wEAAwE;YACxE,qEAAqE;YACrE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAgC,CAAC;YACrD,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,CAAC;gBAAE,OAAO,OAAO,CAAC;QAC3C,CAAC;IACH,CAAC;IAEe,MAAM;QACpB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACxD,CAAC;CAEF"}
1
+ {"version":3,"file":"database-element.js","sourceRoot":"","sources":["../../src/elements/database-element.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,OAAgB,eAA2C,SAAQ,UAAU;IAEjF;+EAC2E;IAC3E,SAAS,CAAiC;IAE1C;;;;;;OAMG;IACH,IAAI,OAAO,CAAC,EAAiC;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,aAAa,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,OAAO;QACT,OAAO,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;;OAOG;IACH,IAAc,QAAQ;QACpB,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED;QACE,KAAK,EAAE,CAAC;QACR,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC7C,CAAC;IAID,iBAAiB;QACf,IAAI,CAAC,uBAAuB,EAAE,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC5C,IAAI,CAAC,OAAO,GAAG,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/E,CAAC;QACD,KAAK,CAAC,iBAAiB,EAAE,CAAC;IAC5B,CAAC;IAED;;;;;;;;OAQG;IACH,uBAAuB;QACrB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC;YAAE,OAAO;QACnE,2EAA2E;QAC3E,0EAA0E;QAC1E,MAAM,IAAI,GAAG,IAA8D,CAAC;QAC5E,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC;QAC9B,OAAO,IAAI,CAAC,OAAO,CAAC;QACpB,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC;QAC1B,CAAC;IACH,CAAC;IAES,mBAAmB;QAC3B,KAAK,MAAM,OAAO,IAAI,uBAAuB,CAAC,IAAI,CAAC,EAAE,CAAC;YACpD,qEAAqE;YACrE,uEAAuE;YACvE,wEAAwE;YACxE,wEAAwE;YACxE,qEAAqE;YACrE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAgC,CAAC;YACrD,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,CAAC;gBAAE,OAAO,OAAO,CAAC;QAC3C,CAAC;IACH,CAAC;IAEe,MAAM;QACpB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACxD,CAAC;CAEF"}
@@ -19,6 +19,9 @@ const plugin = Database.Plugin.create({
19
19
  increment: (t) => {
20
20
  t.resources.count = t.resources.count + 1;
21
21
  },
22
+ setCount: (t, input) => {
23
+ t.resources.count = input.value;
24
+ },
22
25
  },
23
26
  });
24
27
  class _CountElement extends DatabaseElement {
@@ -44,4 +47,7 @@ const _noPublicDatabaseProperty = (el) => {
44
47
  // @ts-expect-error `database` no longer exists; the full db is hard-private
45
48
  void el.database;
46
49
  };
50
+ const _setCountDrivable = null;
51
+ // The plain-args (fire-and-forget) overload also survives, still returning void.
52
+ const _setCountCommit = null;
47
53
  //# sourceMappingURL=database-element.type-test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"database-element.type-test.js","sourceRoot":"","sources":["../../src/elements/database-element.type-test.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD;;;;;;;;GAQG;AAEH,sDAAsD;AAEtD,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAG3C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;IACpC,SAAS,EAAE;QACT,KAAK,EAAE,EAAE,OAAO,EAAE,CAAW,EAAE;KAChC;IACD,YAAY,EAAE;QACZ,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;YACf,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC;QAC5C,CAAC;KACF;CACF,CAAC,CAAC;AAEH,MAAM,aAAc,SAAQ,eAA8B;IACxD,IAAI,MAAM;QACR,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAKD,iFAAiF;AACjF,2EAA2E;AAC3E,yDAAyD;AACzD,MAAM,mBAAmB,GAAG,CAAC,EAAiB,EAAE,EAAe,EAAQ,EAAE;IACvE,EAAE,CAAC,OAAO,GAAG,EAAE,CAAC;AAClB,CAAC,CAAC;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,MAAM,2BAA2B,GAAG,CAAC,EAAiB,EAAQ,EAAE;IAC9D,+EAA+E;IAC/E,EAAE,CAAC,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC;AAC1B,CAAC,CAAC;AAEF,gFAAgF;AAChF,0EAA0E;AAC1E,MAAM,yBAAyB,GAAG,CAAC,EAAiB,EAAQ,EAAE;IAC5D,4EAA4E;IAC5E,KAAK,EAAE,CAAC,QAAQ,CAAC;AACnB,CAAC,CAAC"}
1
+ {"version":3,"file":"database-element.type-test.js","sourceRoot":"","sources":["../../src/elements/database-element.type-test.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD;;;;;;;;GAQG;AAEH,sDAAsD;AAEtD,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAI3C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;IACpC,SAAS,EAAE;QACT,KAAK,EAAE,EAAE,OAAO,EAAE,CAAW,EAAE;KAChC;IACD,YAAY,EAAE;QACZ,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;YACf,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC;QAC5C,CAAC;QACD,QAAQ,EAAE,CAAC,CAAC,EAAE,KAAiC,EAAE,EAAE;YACjD,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QAClC,CAAC;KACF;CACF,CAAC,CAAC;AAEH,MAAM,aAAc,SAAQ,eAA8B;IACxD,IAAI,MAAM;QACR,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAKD,iFAAiF;AACjF,2EAA2E;AAC3E,yDAAyD;AACzD,MAAM,mBAAmB,GAAG,CAAC,EAAiB,EAAE,EAAe,EAAQ,EAAE;IACvE,EAAE,CAAC,OAAO,GAAG,EAAE,CAAC;AAClB,CAAC,CAAC;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,MAAM,2BAA2B,GAAG,CAAC,EAAiB,EAAQ,EAAE;IAC9D,+EAA+E;IAC/E,EAAE,CAAC,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC;AAC1B,CAAC,CAAC;AAEF,gFAAgF;AAChF,0EAA0E;AAC1E,MAAM,yBAAyB,GAAG,CAAC,EAAiB,EAAQ,EAAE;IAC5D,4EAA4E;IAC5E,KAAK,EAAE,CAAC,QAAQ,CAAC;AACnB,CAAC,CAAC;AA8CF,MAAM,iBAAiB,GACrB,IAA0D,CAAC;AAC7D,iFAAiF;AACjF,MAAM,eAAe,GACnB,IAA0D,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./hooks/index.js";
2
2
  export * from "./elements/index.js";
3
3
  export * from "./decorators/index.js";
4
+ export { Template } from "./testing/template/template.js";
4
5
  export type { Vec2 } from "@adobe/data/math";
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@
2
2
  export * from "./hooks/index.js";
3
3
  export * from "./elements/index.js";
4
4
  export * from "./decorators/index.js";
5
+ export { Template } from "./testing/template/template.js";
5
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Query } from "./query.js";
2
+ /**
3
+ * Wrap a presentation's `render(props)` output in a {@link Query} for DOM-free
4
+ * assertions. Throws if given something that isn't a `TemplateResult`, so a
5
+ * presentation that returns `nothing`/`undefined` fails loudly at the call site.
6
+ */
7
+ export declare const from: (result: unknown) => Query;
@@ -0,0 +1,90 @@
1
+ // Duck-typed, DOM-free check for a Lit TemplateResult (or a nested one).
2
+ const isTemplate = (value) => typeof value === "object" &&
3
+ value !== null &&
4
+ Array.isArray(value.strings) &&
5
+ Array.isArray(value.values);
6
+ // Static markup + interpolated primitives, concatenated (recursive). Symbols
7
+ // (e.g. `nothing`), null/undefined, booleans and functions contribute no text.
8
+ const collectText = (value) => {
9
+ if (typeof value === "string")
10
+ return value;
11
+ if (typeof value === "number" || typeof value === "bigint")
12
+ return String(value);
13
+ if (Array.isArray(value))
14
+ return value.map(collectText).join("");
15
+ if (isTemplate(value)) {
16
+ let out = "";
17
+ for (let i = 0; i < value.strings.length; i++) {
18
+ out += value.strings[i];
19
+ if (i < value.values.length)
20
+ out += collectText(value.values[i]);
21
+ }
22
+ return out;
23
+ }
24
+ return "";
25
+ };
26
+ // Every leaf binding value, flattened; nested templates are recursed into but
27
+ // not themselves pushed (those are exposed as `children`).
28
+ const collectValues = (value, into) => {
29
+ if (isTemplate(value)) {
30
+ for (const v of value.values)
31
+ collectValues(v, into);
32
+ }
33
+ else if (Array.isArray(value)) {
34
+ for (const v of value)
35
+ collectValues(v, into);
36
+ }
37
+ else {
38
+ into.push(value);
39
+ }
40
+ };
41
+ // The interpolated child templates directly inside `result` (flattening arrays,
42
+ // but not descending through other templates — `find` handles depth).
43
+ const collectChildren = (result) => {
44
+ const children = [];
45
+ const visit = (value) => {
46
+ if (isTemplate(value))
47
+ children.push(build(value));
48
+ else if (Array.isArray(value))
49
+ value.forEach(visit);
50
+ };
51
+ for (const v of result.values)
52
+ visit(v);
53
+ return children;
54
+ };
55
+ const build = (result) => {
56
+ const text = collectText(result);
57
+ const values = [];
58
+ for (const v of result.values)
59
+ collectValues(v, values);
60
+ const children = collectChildren(result);
61
+ const find = (fragment) => {
62
+ for (const child of children) {
63
+ if (child.text.includes(fragment))
64
+ return child;
65
+ const deep = child.find(fragment);
66
+ if (deep)
67
+ return deep;
68
+ }
69
+ return undefined;
70
+ };
71
+ return {
72
+ text,
73
+ values,
74
+ children,
75
+ has: (fragment) => text.includes(fragment),
76
+ find,
77
+ };
78
+ };
79
+ /**
80
+ * Wrap a presentation's `render(props)` output in a {@link Query} for DOM-free
81
+ * assertions. Throws if given something that isn't a `TemplateResult`, so a
82
+ * presentation that returns `nothing`/`undefined` fails loudly at the call site.
83
+ */
84
+ export const from = (result) => {
85
+ if (!isTemplate(result)) {
86
+ throw new TypeError("Template.from expects a Lit TemplateResult");
87
+ }
88
+ return build(result);
89
+ };
90
+ //# sourceMappingURL=from.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"from.js","sourceRoot":"","sources":["../../../src/testing/template/from.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, it, expect } from "vitest";
3
+ import { html, nothing } from "lit";
4
+ import { Template } from "./template.js";
5
+ describe("Template.from", () => {
6
+ it("collects static markup and interpolated primitives into text", () => {
7
+ const q = Template.from(html `<sp-toast>${"Saved!"} (${3})</sp-toast>`);
8
+ expect(q.has("<sp-toast")).toBe(true);
9
+ expect(q.text).toContain("Saved!");
10
+ expect(q.text).toContain("(3)");
11
+ });
12
+ it("exposes leaf binding values for identity checks (callbacks, nothing, booleans)", () => {
13
+ const onClick = () => { };
14
+ const q = Template.from(html `<sp-button @click=${onClick} ?disabled=${true}>${nothing}</sp-button>`);
15
+ expect(q.values).toContain(onClick);
16
+ expect(q.values).toContain(true);
17
+ expect(q.values).toContain(nothing);
18
+ });
19
+ it("finds interpolated child templates by fragment, recursively", () => {
20
+ const label = "Undo";
21
+ const q = Template.from(html `
22
+ <div>${html `<section>${html `<sp-button>${label}</sp-button>`}</section>`}</div>
23
+ `);
24
+ const button = q.find("<sp-button");
25
+ expect(button).toBeDefined();
26
+ expect(button?.has("Undo")).toBe(true);
27
+ });
28
+ it("flattens values from nested templates and arrays", () => {
29
+ const a = () => { };
30
+ const b = () => { };
31
+ const q = Template.from(html `<ul>${[html `<li @x=${a}></li>`, html `<li @x=${b}></li>`]}</ul>`);
32
+ expect(q.values).toContain(a);
33
+ expect(q.values).toContain(b);
34
+ expect(q.children).toHaveLength(2);
35
+ });
36
+ it("throws when given something that is not a TemplateResult", () => {
37
+ expect(() => Template.from(nothing)).toThrow(TypeError);
38
+ expect(() => Template.from(undefined)).toThrow(TypeError);
39
+ });
40
+ });
41
+ //# sourceMappingURL=from.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"from.test.js","sourceRoot":"","sources":["../../../src/testing/template/from.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { from } from "./from.js";
2
+ export type { Query } from "./query.js";
@@ -0,0 +1,3 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ export { from } from "./from.js";
3
+ //# sourceMappingURL=public.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/testing/template/public.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A DOM-free view over a Lit `TemplateResult`, for asserting on what a pure
3
+ * `render(props)` presentation *declares* — without mounting anything. Built by
4
+ * `Template.from`.
5
+ *
6
+ * - `text` — all static markup plus interpolated primitives, concatenated
7
+ * (recursive). Use for presence: `q.has("<sp-toast")`, `q.text`.
8
+ * - `values` — every leaf binding value, flattened across nested templates in
9
+ * source order (nested templates themselves are excluded — they live in
10
+ * `children`). Use identity checks rather than positions:
11
+ * `expect(q.values).toContain(onClick)`, `.toContain(nothing)`.
12
+ * - `children` — the interpolated child templates (`${html`…`}`), each wrapped.
13
+ * - `has` / `find` — presence sugar and a recursive lookup of the first child
14
+ * template whose `text` includes a fragment.
15
+ *
16
+ * Note: `find` only sees *interpolated* sub-templates; elements written inline
17
+ * in a single `html` literal are part of `text`, not `children`.
18
+ */
19
+ export type Query = {
20
+ readonly text: string;
21
+ readonly values: readonly unknown[];
22
+ readonly children: readonly Query[];
23
+ has(fragment: string): boolean;
24
+ find(fragment: string): Query | undefined;
25
+ };
@@ -0,0 +1,3 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ export {};
3
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../../src/testing/template/query.ts"],"names":[],"mappings":"AAAA,uDAAuD"}
@@ -0,0 +1 @@
1
+ export * as Template from "./public.js";
@@ -0,0 +1,5 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ // DOM-free querying of a Lit `TemplateResult` for presentation unit tests.
3
+ // Pure (no side effects) so it tree-shakes out of production bundles.
4
+ export * as Template from "./public.js";
5
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../../../src/testing/template/template.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,2EAA2E;AAC3E,sEAAsE;AACtE,OAAO,KAAK,QAAQ,MAAM,aAAa,CAAC"}
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@adobe/data-lit",
3
- "version": "0.9.83",
3
+ "version": "0.9.86",
4
4
  "description": "Adobe data Lit bindings - hooks, elements, decorators",
5
5
  "type": "module",
6
6
  "private": false,
7
+ "sideEffects": [
8
+ "**/application-host.js"
9
+ ],
7
10
  "publishConfig": {
8
11
  "registry": "https://registry.npmjs.org/",
9
12
  "access": "public"
10
13
  },
11
14
  "dependencies": {
12
- "@adobe/data": "0.9.83"
15
+ "@adobe/data": "0.9.86"
13
16
  },
14
17
  "peerDependencies": {
15
18
  "lit": "^3.3.1"
@@ -30,6 +33,7 @@
30
33
  "dev": "tsc -b -w",
31
34
  "link": "pnpm build && pnpm link --global",
32
35
  "test": "vitest --run --passWithNoTests",
33
- "publish-public": "pnpm build && pnpm publish --no-git-checks --access public"
36
+ "publish-public": "pnpm build && pnpm publish --no-git-checks --access public",
37
+ "typecheck": "tsc -b"
34
38
  }
35
39
  }
@@ -8,7 +8,8 @@ import { attachDecorator, withHooks } from '../index.js';
8
8
 
9
9
  export abstract class DatabaseElement<P extends Database.Plugin> extends LitElement {
10
10
 
11
- /** Full database, hard-private — invisible to subclasses and external callers. */
11
+ /** Full database, hard-private. Exposed to subclasses only through the
12
+ * restricted {@link service} getter and the {@link database} accessor. */
12
13
  #database!: Database.Plugin.ToDatabase<P>;
13
14
 
14
15
  /**
@@ -27,6 +28,18 @@ export abstract class DatabaseElement<P extends Database.Plugin> extends LitElem
27
28
  return UIService.restrict(this.#database);
28
29
  }
29
30
 
31
+ /**
32
+ * The full, unrestricted database — for imperative-rendering subclasses
33
+ * (a canvas / WebGL draw loop that reads columns synchronously each frame,
34
+ * where the reactive `observe` surface can't help because systems mutate
35
+ * columns in place without firing observers). Presentations and external
36
+ * callers still receive only the restricted {@link service}; this privileged
37
+ * read path is confined to container element authors via `protected`.
38
+ */
39
+ protected get database(): Database.Plugin.ToDatabase<P> {
40
+ return this.#database;
41
+ }
42
+
30
43
  constructor() {
31
44
  super();
32
45
  attachDecorator(this, 'render', withHooks);
@@ -35,6 +48,7 @@ export abstract class DatabaseElement<P extends Database.Plugin> extends LitElem
35
48
  abstract get plugin(): P;
36
49
 
37
50
  connectedCallback(): void {
51
+ this.#upgradeServiceProperty();
38
52
  if (!this.#database) {
39
53
  const ancestor = this.findAncestorService();
40
54
  this.service = ancestor?.extend(this.plugin) ?? Database.create(this.plugin);
@@ -42,6 +56,27 @@ export abstract class DatabaseElement<P extends Database.Plugin> extends LitElem
42
56
  super.connectedCallback();
43
57
  }
44
58
 
59
+ /**
60
+ * A lazy wrapper binds `.service` *before* this element's class is upgraded
61
+ * (the element module is dynamically imported), so the value lands as an own
62
+ * data property that shadows the `service` accessor: `set service` never runs
63
+ * (`#database` stays unset, `database` reads `undefined`) and `get service`
64
+ * returns the raw injected database instead of the restricted view. Reassign
65
+ * the shadowed value through the accessor so the setter runs — `#database` is
66
+ * populated and the UI restriction is actually enforced.
67
+ */
68
+ #upgradeServiceProperty(): void {
69
+ if (!Object.prototype.hasOwnProperty.call(this, "service")) return;
70
+ // The own property was bound by the wrapper as the full database (DI); the
71
+ // `service` accessor's declared type hides that, hence the widening view.
72
+ const self = this as unknown as { service?: Database.Plugin.ToDatabase<P> };
73
+ const injected = self.service;
74
+ delete self.service;
75
+ if (injected !== undefined) {
76
+ this.service = injected;
77
+ }
78
+ }
79
+
45
80
  protected findAncestorService(): Database | void {
46
81
  for (const element of iterateSelfAndAncestors(this)) {
47
82
  // Read each ancestor's `service`. A DatabaseElement returns its full
@@ -13,6 +13,7 @@
13
13
  /* eslint-disable @typescript-eslint/no-unused-vars */
14
14
 
15
15
  import { Database } from "@adobe/data/ecs";
16
+ import type { AsyncArgsProvider } from "@adobe/data/ecs";
16
17
  import { Observe } from "@adobe/data/observe";
17
18
  import { UIService } from "@adobe/data/service";
18
19
  import { DatabaseElement } from "./database-element.js";
@@ -25,6 +26,9 @@ const plugin = Database.Plugin.create({
25
26
  increment: (t) => {
26
27
  t.resources.count = t.resources.count + 1;
27
28
  },
29
+ setCount: (t, input: { readonly value: number }) => {
30
+ t.resources.count = input.value;
31
+ },
28
32
  },
29
33
  });
30
34
 
@@ -94,6 +98,22 @@ type _CheckIncrementReturnsVoid = Assert<
94
98
  Equal<ReturnType<ServiceType["transactions"]["increment"]>, void>
95
99
  >;
96
100
 
101
+ // 7. A transaction with input is exposed as an overload pair. The restriction
102
+ // must preserve the AsyncArgsProvider overload (return rewritten to void) so
103
+ // a UI element can drive a live, single-commit gesture through its
104
+ // restricted `service` — this is exactly what `useDragTransaction`
105
+ // consumes: `(asyncArgs: AsyncArgsProvider<T>) => void`. Regression guard
106
+ // for the overload-erasure bug in UIService's RestrictProperty.
107
+ type DragConsumer<T> = (asyncArgs: AsyncArgsProvider<T>) => void;
108
+ const _setCountDrivable: DragConsumer<{ readonly value: number }> =
109
+ null as unknown as ServiceType["transactions"]["setCount"];
110
+ // The plain-args (fire-and-forget) overload also survives, still returning void.
111
+ const _setCountCommit: (arg: { readonly value: number }) => void =
112
+ null as unknown as ServiceType["transactions"]["setCount"];
113
+ type _CheckSetCountAsyncVoid = Assert<
114
+ Equal<ReturnType<ServiceType["transactions"]["setCount"]>, void>
115
+ >;
116
+
97
117
  // ----------------------------------------------------------------------------
98
118
  // Local helpers (kept inline so this file stays a self-contained type test).
99
119
  // ----------------------------------------------------------------------------
package/src/index.ts CHANGED
@@ -3,4 +3,5 @@
3
3
  export * from "./hooks/index.js";
4
4
  export * from "./elements/index.js";
5
5
  export * from "./decorators/index.js";
6
+ export { Template } from "./testing/template/template.js";
6
7
  export type { Vec2 } from "@adobe/data/math";
@@ -0,0 +1,47 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, it, expect } from "vitest";
3
+ import { html, nothing } from "lit";
4
+ import { Template } from "./template.js";
5
+
6
+ describe("Template.from", () => {
7
+ it("collects static markup and interpolated primitives into text", () => {
8
+ const q = Template.from(html`<sp-toast>${"Saved!"} (${3})</sp-toast>`);
9
+ expect(q.has("<sp-toast")).toBe(true);
10
+ expect(q.text).toContain("Saved!");
11
+ expect(q.text).toContain("(3)");
12
+ });
13
+
14
+ it("exposes leaf binding values for identity checks (callbacks, nothing, booleans)", () => {
15
+ const onClick = () => {};
16
+ const q = Template.from(
17
+ html`<sp-button @click=${onClick} ?disabled=${true}>${nothing}</sp-button>`,
18
+ );
19
+ expect(q.values).toContain(onClick);
20
+ expect(q.values).toContain(true);
21
+ expect(q.values).toContain(nothing);
22
+ });
23
+
24
+ it("finds interpolated child templates by fragment, recursively", () => {
25
+ const label = "Undo";
26
+ const q = Template.from(html`
27
+ <div>${html`<section>${html`<sp-button>${label}</sp-button>`}</section>`}</div>
28
+ `);
29
+ const button = q.find("<sp-button");
30
+ expect(button).toBeDefined();
31
+ expect(button?.has("Undo")).toBe(true);
32
+ });
33
+
34
+ it("flattens values from nested templates and arrays", () => {
35
+ const a = () => {};
36
+ const b = () => {};
37
+ const q = Template.from(html`<ul>${[html`<li @x=${a}></li>`, html`<li @x=${b}></li>`]}</ul>`);
38
+ expect(q.values).toContain(a);
39
+ expect(q.values).toContain(b);
40
+ expect(q.children).toHaveLength(2);
41
+ });
42
+
43
+ it("throws when given something that is not a TemplateResult", () => {
44
+ expect(() => Template.from(nothing)).toThrow(TypeError);
45
+ expect(() => Template.from(undefined)).toThrow(TypeError);
46
+ });
47
+ });
@@ -0,0 +1,86 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Query } from "./query.js";
3
+
4
+ type TemplateLike = { readonly strings: readonly string[]; readonly values: readonly unknown[] };
5
+
6
+ // Duck-typed, DOM-free check for a Lit TemplateResult (or a nested one).
7
+ const isTemplate = (value: unknown): value is TemplateLike =>
8
+ typeof value === "object" &&
9
+ value !== null &&
10
+ Array.isArray((value as { strings?: unknown }).strings) &&
11
+ Array.isArray((value as { values?: unknown }).values);
12
+
13
+ // Static markup + interpolated primitives, concatenated (recursive). Symbols
14
+ // (e.g. `nothing`), null/undefined, booleans and functions contribute no text.
15
+ const collectText = (value: unknown): string => {
16
+ if (typeof value === "string") return value;
17
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
18
+ if (Array.isArray(value)) return value.map(collectText).join("");
19
+ if (isTemplate(value)) {
20
+ let out = "";
21
+ for (let i = 0; i < value.strings.length; i++) {
22
+ out += value.strings[i];
23
+ if (i < value.values.length) out += collectText(value.values[i]);
24
+ }
25
+ return out;
26
+ }
27
+ return "";
28
+ };
29
+
30
+ // Every leaf binding value, flattened; nested templates are recursed into but
31
+ // not themselves pushed (those are exposed as `children`).
32
+ const collectValues = (value: unknown, into: unknown[]): void => {
33
+ if (isTemplate(value)) {
34
+ for (const v of value.values) collectValues(v, into);
35
+ } else if (Array.isArray(value)) {
36
+ for (const v of value) collectValues(v, into);
37
+ } else {
38
+ into.push(value);
39
+ }
40
+ };
41
+
42
+ // The interpolated child templates directly inside `result` (flattening arrays,
43
+ // but not descending through other templates — `find` handles depth).
44
+ const collectChildren = (result: TemplateLike): Query[] => {
45
+ const children: Query[] = [];
46
+ const visit = (value: unknown): void => {
47
+ if (isTemplate(value)) children.push(build(value));
48
+ else if (Array.isArray(value)) value.forEach(visit);
49
+ };
50
+ for (const v of result.values) visit(v);
51
+ return children;
52
+ };
53
+
54
+ const build = (result: TemplateLike): Query => {
55
+ const text = collectText(result);
56
+ const values: unknown[] = [];
57
+ for (const v of result.values) collectValues(v, values);
58
+ const children = collectChildren(result);
59
+ const find = (fragment: string): Query | undefined => {
60
+ for (const child of children) {
61
+ if (child.text.includes(fragment)) return child;
62
+ const deep = child.find(fragment);
63
+ if (deep) return deep;
64
+ }
65
+ return undefined;
66
+ };
67
+ return {
68
+ text,
69
+ values,
70
+ children,
71
+ has: (fragment) => text.includes(fragment),
72
+ find,
73
+ };
74
+ };
75
+
76
+ /**
77
+ * Wrap a presentation's `render(props)` output in a {@link Query} for DOM-free
78
+ * assertions. Throws if given something that isn't a `TemplateResult`, so a
79
+ * presentation that returns `nothing`/`undefined` fails loudly at the call site.
80
+ */
81
+ export const from = (result: unknown): Query => {
82
+ if (!isTemplate(result)) {
83
+ throw new TypeError("Template.from expects a Lit TemplateResult");
84
+ }
85
+ return build(result);
86
+ };
@@ -0,0 +1,3 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ export { from } from "./from.js";
3
+ export type { Query } from "./query.js";