p-elements-core 1.2.30 → 1.2.32-rc-10
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/.editorconfig +17 -17
- package/.gitlab-ci.yml +18 -18
- package/CHANGELOG.md +201 -0
- package/demo/sample.js +1 -1
- package/demo/screen.css +16 -16
- package/demo/theme.css +1 -0
- package/dist/p-elements-core-modern.js +1 -1
- package/dist/p-elements-core.js +1 -1
- package/docs/package-lock.json +6897 -6897
- package/docs/package.json +27 -27
- package/docs/src/404.md +8 -8
- package/docs/src/_data/demos/hello-world/hello-world.tsx +35 -35
- package/docs/src/_data/demos/hello-world/index.html +10 -10
- package/docs/src/_data/demos/hello-world/project.json +7 -7
- package/docs/src/_data/demos/timer/demo-timer.tsx +120 -120
- package/docs/src/_data/demos/timer/icons.tsx +62 -62
- package/docs/src/_data/demos/timer/index.html +12 -12
- package/docs/src/_data/demos/timer/project.json +8 -8
- package/docs/src/_data/global.js +13 -13
- package/docs/src/_data/helpers.js +19 -19
- package/docs/src/_includes/layouts/base.njk +30 -30
- package/docs/src/_includes/layouts/playground.njk +40 -40
- package/docs/src/_includes/partials/app-header.njk +8 -8
- package/docs/src/_includes/partials/head.njk +14 -14
- package/docs/src/_includes/partials/nav.njk +19 -19
- package/docs/src/_includes/partials/top-nav.njk +51 -51
- package/docs/src/documentation/custom-element.md +221 -221
- package/docs/src/documentation/decorators/bind.md +71 -71
- package/docs/src/documentation/decorators/custom-element-config.md +63 -63
- package/docs/src/documentation/decorators/property.md +83 -83
- package/docs/src/documentation/decorators/query.md +66 -66
- package/docs/src/documentation/decorators/render-property-on-set.md +60 -60
- package/docs/src/documentation/decorators.md +9 -9
- package/docs/src/documentation/reactive-properties.md +53 -53
- package/docs/src/index.d.ts +25 -25
- package/docs/src/index.md +3 -3
- package/docs/src/scripts/components/app-mode-switch/app-mode-switch.css +78 -78
- package/docs/src/scripts/components/app-mode-switch/app-mode-switch.tsx +166 -166
- package/docs/src/scripts/components/app-playground/app-playground.tsx +189 -189
- package/docs/tsconfig.json +22 -22
- package/index.html +15 -2
- package/p-elements-core.d.ts +12 -3
- package/package.json +11 -4
- package/readme.md +206 -206
- package/src/custom-element-controller.test.ts +226 -0
- package/src/custom-element-controller.ts +31 -31
- package/src/custom-element.test.ts +906 -0
- package/src/custom-element.ts +471 -188
- package/src/custom-style-element.ts +4 -1
- package/src/decorators/bind.test.ts +163 -0
- package/src/decorators/bind.ts +46 -46
- package/src/decorators/custom-element-config.ts +17 -17
- package/src/decorators/property.test.ts +279 -0
- package/src/decorators/property.ts +822 -150
- package/src/decorators/query.test.ts +146 -0
- package/src/decorators/query.ts +12 -12
- package/src/decorators/render-property-on-set.ts +3 -3
- package/src/helpers/css.test.ts +150 -0
- package/src/helpers/css.ts +71 -71
- package/src/maquette/cache.test.ts +150 -0
- package/src/maquette/cache.ts +35 -35
- package/src/maquette/dom.test.ts +263 -0
- package/src/maquette/dom.ts +115 -115
- package/src/maquette/h.test.ts +165 -0
- package/src/maquette/h.ts +100 -100
- package/src/maquette/index.ts +12 -12
- package/src/maquette/interfaces.ts +536 -536
- package/src/maquette/jsx.ts +61 -61
- package/src/maquette/mapping.test.ts +294 -0
- package/src/maquette/mapping.ts +56 -56
- package/src/maquette/maquette.test.ts +493 -0
- package/src/maquette/projection.test.ts +366 -0
- package/src/maquette/projection.ts +666 -666
- package/src/maquette/projector.test.ts +351 -0
- package/src/maquette/projector.ts +200 -200
- package/src/sample/mixin/highlight.tsx +33 -32
- package/src/sample/sample.tsx +167 -7
- package/src/test-setup.ts +85 -0
- package/src/test-utils.ts +223 -0
- package/tsconfig.json +1 -0
- package/vitest.config.ts +41 -0
- package/webpack.config.js +1 -1
package/src/sample/sample.tsx
CHANGED
|
@@ -1,20 +1,80 @@
|
|
|
1
1
|
import { Highlightable } from "./mixin/highlight";
|
|
2
2
|
import anime from "animejs";
|
|
3
3
|
|
|
4
|
-
import {customElementConfig as CustomElementConfig} from "../decorators/custom-element-config";
|
|
4
|
+
import {customElementConfig, customElementConfig as CustomElementConfig} from "../decorators/custom-element-config";
|
|
5
5
|
import {property as Property, AttributeConverter} from "../decorators/property";
|
|
6
6
|
import {propertyRenderOnSet as PropertyRenderOnSet} from "../decorators/render-property-on-set";
|
|
7
7
|
import {query as Query} from "../decorators/query";
|
|
8
8
|
import {CustomElement} from "../custom-element";
|
|
9
9
|
import {CustomElementController} from "../custom-element-controller";
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
@customElementConfig({tagName: "render-on-set"})
|
|
13
|
+
export class RenderOnSet extends CustomElement {
|
|
14
|
+
|
|
15
|
+
@PropertyRenderOnSet
|
|
16
|
+
public name: string = "foo";
|
|
17
|
+
|
|
18
|
+
render() {
|
|
19
|
+
return <div>Render on set: {this.name}</div>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#initDone = false;
|
|
23
|
+
|
|
24
|
+
connectedCallback(): void {
|
|
25
|
+
|
|
26
|
+
if (this.#initDone) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
this.#initDone = true;
|
|
30
|
+
let template = this.templateFromString(`<style>div{color: lime}</style><div />`);
|
|
31
|
+
this.shadowRoot.appendChild(template);
|
|
32
|
+
this.createProjector(this.shadowRoot.querySelector("div"), this.render);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@CustomElementConfig({
|
|
39
|
+
tagName: "p-bugs-03",
|
|
40
|
+
})
|
|
41
|
+
export class PBugs03 extends CustomElement {
|
|
42
|
+
static style = `/* */`;
|
|
43
|
+
|
|
44
|
+
@PropertyRenderOnSet
|
|
45
|
+
private _zero: number = 0;
|
|
46
|
+
|
|
47
|
+
private _renderCount: number = 0;
|
|
48
|
+
|
|
49
|
+
private alwaysZero() {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private get zero(): number {
|
|
54
|
+
this._zero = this.alwaysZero();
|
|
55
|
+
return this._zero;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Bug: Updating a @RenderOnSet property with same value triggers useless re-render
|
|
59
|
+
|
|
60
|
+
render() {
|
|
61
|
+
this._renderCount++;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div>
|
|
65
|
+
Value of Zero: {this.zero} - Renders: {this._renderCount}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
11
71
|
@CustomElementConfig({
|
|
12
72
|
tagName: "my-button",
|
|
13
73
|
})
|
|
14
74
|
export class MyButton extends CustomElement {
|
|
15
75
|
static style = `.foo{background-color: red; color: white; border: 0px;}`;
|
|
16
76
|
static delegatesFocus = true;
|
|
17
|
-
render
|
|
77
|
+
render(){
|
|
18
78
|
return <button class="foo"><slot></slot></button>;
|
|
19
79
|
}
|
|
20
80
|
}
|
|
@@ -22,7 +82,7 @@ export class MyButton extends CustomElement {
|
|
|
22
82
|
@CustomElementConfig({
|
|
23
83
|
tagName: "my-greetings",
|
|
24
84
|
})
|
|
25
|
-
export class MyGreetings extends Highlightable(class extends CustomElement { }) {
|
|
85
|
+
export class MyGreetings extends Highlightable(class extends CustomElement { render() { return <div></div> } }) {
|
|
26
86
|
|
|
27
87
|
static isFormAssociated = true;
|
|
28
88
|
|
|
@@ -94,7 +154,7 @@ export class MyGreetings extends Highlightable(class extends CustomElement { })
|
|
|
94
154
|
console.log({ e, p: this });
|
|
95
155
|
};
|
|
96
156
|
|
|
97
|
-
|
|
157
|
+
render(){
|
|
98
158
|
return (
|
|
99
159
|
<div>
|
|
100
160
|
<input type="checkbox" checked={this.name === "Peter"} />
|
|
@@ -139,8 +199,8 @@ export class MyGreetings extends Highlightable(class extends CustomElement { })
|
|
|
139
199
|
afterCreate={this.peterCreate}
|
|
140
200
|
class="is-peter"
|
|
141
201
|
eventListeners={[["click", this.onPeterClick]]}
|
|
142
|
-
enterAnimation={this.enterAnimation}
|
|
143
|
-
exitAnimation={this.exitAnimation}
|
|
202
|
+
enterAnimation={(element, properties) => this.enterAnimation(element, properties)}
|
|
203
|
+
exitAnimation={(element, removeDomNodeFunction, properties) => this.exitAnimation(element, removeDomNodeFunction, properties)}
|
|
144
204
|
>
|
|
145
205
|
<p>Hello Peter</p>
|
|
146
206
|
<img
|
|
@@ -165,6 +225,7 @@ export class MyGreetings extends Highlightable(class extends CustomElement { })
|
|
|
165
225
|
}
|
|
166
226
|
|
|
167
227
|
connectedCallback() {
|
|
228
|
+
super.connectedCallback();
|
|
168
229
|
let template = this.templateFromString(`
|
|
169
230
|
<style>
|
|
170
231
|
|
|
@@ -308,7 +369,7 @@ class PFoo extends CustomElement {
|
|
|
308
369
|
@Property({ attribute: "items", type: "object", reflect: true, converter: stringArrayConverter })
|
|
309
370
|
items: string[] = ["foo", "bar"];
|
|
310
371
|
|
|
311
|
-
@Property({ type: "string", attribute: "nick-name"
|
|
372
|
+
@Property({ type: "string", attribute: "nick-name"})
|
|
312
373
|
nickName: string = "de prutser";
|
|
313
374
|
|
|
314
375
|
shouldUpdate(property, oldValue, newValue) {
|
|
@@ -359,9 +420,108 @@ class SuperAnchorElement extends HTMLAnchorElement {
|
|
|
359
420
|
}
|
|
360
421
|
}
|
|
361
422
|
public connectedCallback() {
|
|
423
|
+
|
|
362
424
|
this.classList.add("super");
|
|
363
425
|
this.style.color = "red";
|
|
364
426
|
}
|
|
365
427
|
}
|
|
366
428
|
|
|
367
429
|
window.customElements.define("super-a", SuperAnchorElement, { extends: "a" });
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
// Bring your own projector in non shadow root element
|
|
433
|
+
@CustomElementConfig({
|
|
434
|
+
tagName: "bring-your-own-projector",
|
|
435
|
+
})
|
|
436
|
+
export class BringYourOwnProjector extends CustomElement {
|
|
437
|
+
|
|
438
|
+
constructor() {
|
|
439
|
+
super();
|
|
440
|
+
this.initProjector();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
init() {
|
|
444
|
+
console.log("This is a lifecycle for BringYourOwnProjector");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
initProjector() {
|
|
448
|
+
this.rootElement = document.createElement("div");
|
|
449
|
+
this.rootElement.classList.add("root");
|
|
450
|
+
this.createProjector(this.rootElement, this.render);
|
|
451
|
+
console.log("init");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@PropertyRenderOnSet
|
|
455
|
+
name: string = "foo";
|
|
456
|
+
|
|
457
|
+
#_doneSOmething = false;
|
|
458
|
+
|
|
459
|
+
get doneSomething() {
|
|
460
|
+
return this.#_doneSOmething;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
doItNow(value) {
|
|
464
|
+
this.#_doneSOmething = value;
|
|
465
|
+
this.renderNow();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
rootElement: HTMLDivElement;
|
|
469
|
+
|
|
470
|
+
render() {
|
|
471
|
+
return <div>Hello {this.name} {this.doneSomething && <p>We did it!!</p>}</div>;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
connectedCallback() {
|
|
475
|
+
this.appendChild(this.rootElement);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Bring your own projector in non shadow root element
|
|
481
|
+
@CustomElementConfig({
|
|
482
|
+
tagName: "bring-your-own-projector-in-shadow-root",
|
|
483
|
+
})
|
|
484
|
+
export class BringYourOwnProjectorInShadowRoot extends CustomElement {
|
|
485
|
+
|
|
486
|
+
constructor() {
|
|
487
|
+
super();
|
|
488
|
+
this.initProjector();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
init() {
|
|
492
|
+
console.log("This is a lifecycle for BringYourOwnProjector");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
initProjector() {
|
|
496
|
+
this.attachShadow({ mode: "open" });
|
|
497
|
+
this.rootElement = document.createElement("div");
|
|
498
|
+
this.rootElement.classList.add("root");
|
|
499
|
+
this.createProjector(this.rootElement, this.render);
|
|
500
|
+
console.log("init");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
@PropertyRenderOnSet
|
|
504
|
+
name: string = "foo";
|
|
505
|
+
|
|
506
|
+
#_doneSOmething = false;
|
|
507
|
+
|
|
508
|
+
get doneSomething() {
|
|
509
|
+
return this.#_doneSOmething;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
doItNow(value) {
|
|
513
|
+
this.#_doneSOmething = value;
|
|
514
|
+
this.renderNow();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
rootElement: HTMLDivElement;
|
|
518
|
+
|
|
519
|
+
render() {
|
|
520
|
+
return <div>Hello {this.name} {this.doneSomething && <p>We did it!!</p>}</div>;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
connectedCallback() {
|
|
524
|
+
this.shadowRoot.appendChild(this.rootElement);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test setup file - Vitest configuration
|
|
3
|
+
* Configures polyfills and global test utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { expect, afterEach, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
// Import Maquette at the top level for type definitions
|
|
9
|
+
import * as Maquette from './maquette/index.js';
|
|
10
|
+
|
|
11
|
+
// Declare global Maquette if in browser environment
|
|
12
|
+
declare global {
|
|
13
|
+
interface Window {
|
|
14
|
+
Maquette: typeof Maquette;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Only run setup code if we're in a browser environment
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
// Set up Maquette global
|
|
21
|
+
(window as any).Maquette = Maquette;
|
|
22
|
+
|
|
23
|
+
// Import and register CustomStyleElement for CSS @apply polyfill
|
|
24
|
+
if (typeof HTMLLinkElement !== 'undefined') {
|
|
25
|
+
import('./custom-style-element.js').catch(() => {
|
|
26
|
+
// Ignore if import fails in test environment
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extend Chai matchers for better custom element testing
|
|
32
|
+
declare global {
|
|
33
|
+
namespace Chai {
|
|
34
|
+
interface Assertion {
|
|
35
|
+
shadowRoot: ShadowRoot;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Store for tracking registered custom element names to prevent conflicts
|
|
41
|
+
const registeredElements = new Set<string>();
|
|
42
|
+
const elementsCreated: HTMLElement[] = [];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generate a unique custom element tag name for testing
|
|
46
|
+
* Ensures no conflicts between test cases
|
|
47
|
+
*/
|
|
48
|
+
export function generateUniqueTagName(prefix: string = 'test-element'): string {
|
|
49
|
+
const timestamp = Date.now();
|
|
50
|
+
const random = Math.random().toString(36).substr(2, 9);
|
|
51
|
+
const tagName = `${prefix}-${timestamp}-${random}`.toLowerCase();
|
|
52
|
+
|
|
53
|
+
registeredElements.add(tagName);
|
|
54
|
+
return tagName;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clean up helper - unregister all test elements after tests
|
|
59
|
+
* Note: CustomElementRegistry doesn't support unregistration,
|
|
60
|
+
* so we track what was registered for uniqueness
|
|
61
|
+
*/
|
|
62
|
+
export function resetRegisteredElements(): void {
|
|
63
|
+
registeredElements.clear();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Track created elements for cleanup
|
|
68
|
+
*/
|
|
69
|
+
export function trackElement(el: HTMLElement): void {
|
|
70
|
+
elementsCreated.push(el);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clean up all created elements after each test
|
|
75
|
+
*/
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
elementsCreated.forEach(el => {
|
|
78
|
+
if (el.parentElement) {
|
|
79
|
+
el.parentElement.removeChild(el);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
elementsCreated.length = 0;
|
|
83
|
+
});
|
|
84
|
+
// Global test timeout for async operations
|
|
85
|
+
export const DEFAULT_TEST_TIMEOUT = 5000;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test utilities for p-elements-core testing with Vitest
|
|
3
|
+
* Provides helpers for creating and testing custom elements
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CustomElement } from './custom-element.js';
|
|
7
|
+
import { customElementConfig } from './decorators/custom-element-config.js';
|
|
8
|
+
import { generateUniqueTagName, trackElement } from './test-setup.js';
|
|
9
|
+
import type { VNode } from './maquette/interfaces.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wait for an element to complete its update cycle
|
|
13
|
+
*/
|
|
14
|
+
export async function waitForRender(element: CustomElement): Promise<void> {
|
|
15
|
+
// Wait for shadowRoot to be created (async in requestAnimationFrame)
|
|
16
|
+
let attempts = 0;
|
|
17
|
+
const maxAttempts = 100;
|
|
18
|
+
while (!element.shadowRoot && attempts < maxAttempts) {
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
20
|
+
attempts++;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!element.shadowRoot && attempts >= maxAttempts) {
|
|
24
|
+
console.warn('waitForRender: shadowRoot was not created after max attempts');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Wait for projector to render (also async in requestAnimationFrame)
|
|
28
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(undefined))));
|
|
29
|
+
|
|
30
|
+
// Then wait for update to complete
|
|
31
|
+
await element.updateComplete;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create and register a test custom element
|
|
36
|
+
* Returns the tag name for the registered element
|
|
37
|
+
*/
|
|
38
|
+
export function defineTestElement(
|
|
39
|
+
render: () => VNode,
|
|
40
|
+
options: {
|
|
41
|
+
tagName?: string;
|
|
42
|
+
isFormAssociated?: boolean;
|
|
43
|
+
delegatesFocus?: boolean;
|
|
44
|
+
style?: string;
|
|
45
|
+
extends?: string;
|
|
46
|
+
} = {}
|
|
47
|
+
): string {
|
|
48
|
+
const tagName = options.tagName || generateUniqueTagName();
|
|
49
|
+
|
|
50
|
+
@customElementConfig({
|
|
51
|
+
tagName,
|
|
52
|
+
options: options.extends ? { extends: options.extends } : undefined,
|
|
53
|
+
})
|
|
54
|
+
class TestElement extends CustomElement {
|
|
55
|
+
static style = options.style || '';
|
|
56
|
+
static isFormAssociated = options.isFormAssociated || false;
|
|
57
|
+
static delegatesFocus = options.delegatesFocus || false;
|
|
58
|
+
|
|
59
|
+
render() {
|
|
60
|
+
return render();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return tagName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a simple test element directly in the DOM
|
|
69
|
+
*/
|
|
70
|
+
export async function createTestElement<T extends HTMLElement = HTMLElement>(
|
|
71
|
+
tagName: string,
|
|
72
|
+
attributes: Record<string, any> = {}
|
|
73
|
+
): Promise<T> {
|
|
74
|
+
const el = document.createElement(tagName) as T;
|
|
75
|
+
|
|
76
|
+
// Set attributes
|
|
77
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
78
|
+
if (typeof value === 'boolean') {
|
|
79
|
+
if (value) {
|
|
80
|
+
el.setAttribute(key, '');
|
|
81
|
+
}
|
|
82
|
+
} else if (value !== null && value !== undefined) {
|
|
83
|
+
el.setAttribute(key, String(value));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Add to body for connection
|
|
88
|
+
document.body.appendChild(el);
|
|
89
|
+
trackElement(el);
|
|
90
|
+
|
|
91
|
+
// Wait for render if it's a CustomElement
|
|
92
|
+
if (el instanceof CustomElement) {
|
|
93
|
+
await waitForRender(el as CustomElement);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return el;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get property descriptor from an element (helps test decorator-generated properties)
|
|
101
|
+
*/
|
|
102
|
+
export function getPropertyDescriptor(
|
|
103
|
+
element: any,
|
|
104
|
+
propertyName: string
|
|
105
|
+
): PropertyDescriptor | undefined {
|
|
106
|
+
let proto = Object.getPrototypeOf(element);
|
|
107
|
+
|
|
108
|
+
while (proto) {
|
|
109
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, propertyName);
|
|
110
|
+
if (descriptor) {
|
|
111
|
+
return descriptor;
|
|
112
|
+
}
|
|
113
|
+
proto = Object.getPrototypeOf(proto);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Wait for a condition to be true
|
|
121
|
+
*/
|
|
122
|
+
export async function waitFor(
|
|
123
|
+
condition: () => boolean,
|
|
124
|
+
timeout: number = 1000,
|
|
125
|
+
interval: number = 10
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
const startTime = Date.now();
|
|
128
|
+
|
|
129
|
+
while (!condition()) {
|
|
130
|
+
if (Date.now() - startTime > timeout) {
|
|
131
|
+
throw new Error('Timeout waiting for condition');
|
|
132
|
+
}
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Simulate an attribute change on an element
|
|
139
|
+
*/
|
|
140
|
+
export function setAttributeAndNotify(
|
|
141
|
+
element: HTMLElement & { attributeChangedCallback?: Function },
|
|
142
|
+
name: string,
|
|
143
|
+
value: string | null
|
|
144
|
+
): void {
|
|
145
|
+
const oldValue = element.getAttribute(name);
|
|
146
|
+
|
|
147
|
+
if (value === null) {
|
|
148
|
+
element.removeAttribute(name);
|
|
149
|
+
} else {
|
|
150
|
+
element.setAttribute(name, value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Manually trigger attributeChangedCallback if it exists
|
|
154
|
+
if (element.attributeChangedCallback) {
|
|
155
|
+
element.attributeChangedCallback(name, oldValue, value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Assert that a VNode has specific properties
|
|
161
|
+
*/
|
|
162
|
+
export function assertVNode(
|
|
163
|
+
vnode: VNode,
|
|
164
|
+
expected: Partial<{
|
|
165
|
+
vnodeSelector: string;
|
|
166
|
+
text?: string;
|
|
167
|
+
children?: VNode[];
|
|
168
|
+
}>
|
|
169
|
+
): void {
|
|
170
|
+
if (expected.vnodeSelector) {
|
|
171
|
+
if (vnode.vnodeSelector !== expected.vnodeSelector) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Expected vnodeSelector "${expected.vnodeSelector}" but got "${vnode.vnodeSelector}"`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (expected.text !== undefined) {
|
|
179
|
+
if (vnode.text !== expected.text) {
|
|
180
|
+
throw new Error(`Expected text "${expected.text}" but got "${vnode.text}"`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (expected.children) {
|
|
185
|
+
if (!vnode.children || vnode.children.length !== expected.children.length) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Expected ${expected.children.length} children but got ${vnode.children?.length || 0}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get the shadow root of an element, throwing if not found
|
|
195
|
+
*/
|
|
196
|
+
export function getShadowRoot(element: HTMLElement): ShadowRoot {
|
|
197
|
+
if (!element.shadowRoot) {
|
|
198
|
+
throw new Error('Element does not have a shadow root');
|
|
199
|
+
}
|
|
200
|
+
return element.shadowRoot;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Query a selector within an element's shadow DOM
|
|
205
|
+
*/
|
|
206
|
+
export function shadowQuery<T extends Element = Element>(
|
|
207
|
+
element: HTMLElement,
|
|
208
|
+
selector: string
|
|
209
|
+
): T | null {
|
|
210
|
+
return getShadowRoot(element).querySelector<T>(selector);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Query all selectors within an element's shadow DOM
|
|
215
|
+
*/
|
|
216
|
+
export function shadowQueryAll<T extends Element = Element>(
|
|
217
|
+
element: HTMLElement,
|
|
218
|
+
selector: string
|
|
219
|
+
): NodeListOf<T> {
|
|
220
|
+
return getShadowRoot(element).querySelectorAll<T>(selector);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
package/tsconfig.json
CHANGED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import { playwright } from '@vitest/browser-playwright';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
browser: {
|
|
7
|
+
enabled: true,
|
|
8
|
+
provider: playwright(),
|
|
9
|
+
headless: process.env.HEADED !== 'true',
|
|
10
|
+
instances: [
|
|
11
|
+
{
|
|
12
|
+
browser: 'chromium',
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
globals: true,
|
|
17
|
+
setupFiles: ['./src/test-setup.ts'],
|
|
18
|
+
include: ['src/**/*.test.{ts,tsx}'],
|
|
19
|
+
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'],
|
|
20
|
+
coverage: {
|
|
21
|
+
provider: 'v8',
|
|
22
|
+
reporter: ['text', 'json', 'html', 'lcov'],
|
|
23
|
+
reportsDirectory: 'coverage',
|
|
24
|
+
exclude: [
|
|
25
|
+
'node_modules/',
|
|
26
|
+
'src/test-setup.ts',
|
|
27
|
+
'src/test-utils.ts',
|
|
28
|
+
'src/**/*.test.ts',
|
|
29
|
+
],
|
|
30
|
+
thresholds: {
|
|
31
|
+
lines: 70,
|
|
32
|
+
functions: 70,
|
|
33
|
+
branches: 60,
|
|
34
|
+
statements: 70,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
testTimeout: 5000,
|
|
38
|
+
hookTimeout: 5000,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|