@adobe/data-lit 0.9.83 → 0.9.84
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.
- package/dist/elements/database-element.d.ts +9 -0
- package/dist/elements/database-element.js +35 -1
- package/dist/elements/database-element.js.map +1 -1
- package/dist/elements/database-element.type-test.js +6 -0
- package/dist/elements/database-element.type-test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/testing/template/from.d.ts +7 -0
- package/dist/testing/template/from.js +90 -0
- package/dist/testing/template/from.js.map +1 -0
- package/dist/testing/template/from.test.d.ts +1 -0
- package/dist/testing/template/from.test.js +41 -0
- package/dist/testing/template/from.test.js.map +1 -0
- package/dist/testing/template/public.d.ts +2 -0
- package/dist/testing/template/public.js +3 -0
- package/dist/testing/template/public.js.map +1 -0
- package/dist/testing/template/query.d.ts +25 -0
- package/dist/testing/template/query.js +3 -0
- package/dist/testing/template/query.js.map +1 -0
- package/dist/testing/template/template.d.ts +1 -0
- package/dist/testing/template/template.js +5 -0
- package/dist/testing/template/template.js.map +1 -0
- package/package.json +7 -3
- package/src/elements/database-element.ts +36 -1
- package/src/elements/database-element.type-test.ts +20 -0
- package/src/index.ts +1 -0
- package/src/testing/template/from.test.ts +47 -0
- package/src/testing/template/from.ts +86 -0
- package/src/testing/template/public.ts +3 -0
- package/src/testing/template/query.ts +27 -0
- package/src/testing/template/template.ts +4 -0
- 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
|
|
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
|
|
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;
|
|
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
package/dist/index.js
CHANGED
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 @@
|
|
|
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 @@
|
|
|
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.
|
|
3
|
+
"version": "0.9.84",
|
|
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.
|
|
15
|
+
"@adobe/data": "0.9.84"
|
|
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
|
|
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
|
@@ -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
|
+
};
|