p-elements-core 1.2.32-rc1 → 1.2.32-rc11
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/CHANGELOG.md +201 -0
- package/demo/sample.js +1 -1
- package/demo/theme.css +1 -0
- package/dist/p-elements-core-modern.js +1 -1
- package/dist/p-elements-core.js +1 -1
- package/index.html +15 -2
- package/p-elements-core.d.ts +11 -1
- package/package.json +10 -3
- package/src/custom-element-controller.test.ts +226 -0
- package/src/custom-element.test.ts +906 -0
- package/src/custom-element.ts +74 -17
- package/src/custom-style-element.ts +4 -1
- package/src/decorators/bind.test.ts +163 -0
- package/src/decorators/property.test.ts +279 -0
- package/src/decorators/property.ts +176 -10
- package/src/decorators/query.test.ts +146 -0
- package/src/helpers/css.test.ts +150 -0
- package/src/maquette/cache.test.ts +150 -0
- package/src/maquette/dom.test.ts +263 -0
- package/src/maquette/h.test.ts +165 -0
- package/src/maquette/mapping.test.ts +294 -0
- package/src/maquette/maquette.test.ts +493 -0
- package/src/maquette/projection.test.ts +366 -0
- package/src/maquette/projector.test.ts +351 -0
- package/src/maquette/projector.ts +6 -1
- package/src/sample/sample.tsx +167 -8
- 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
|
@@ -63,6 +63,8 @@ export interface PropertyOptions {
|
|
|
63
63
|
*/
|
|
64
64
|
reflect?: boolean;
|
|
65
65
|
|
|
66
|
+
readonly?: boolean;
|
|
67
|
+
|
|
66
68
|
/**
|
|
67
69
|
* Custom converter for complex attribute/property transformations.
|
|
68
70
|
* @type {AttributeConverter<any>}
|
|
@@ -76,7 +78,7 @@ export interface PropertyOptions {
|
|
|
76
78
|
*
|
|
77
79
|
* @typedef {PropertyOptions & { name: string }} PropertyOptionsWithName
|
|
78
80
|
*/
|
|
79
|
-
export type PropertyOptionsWithName = PropertyOptions & {name: string};
|
|
81
|
+
export type PropertyOptionsWithName = PropertyOptions & { name: string };
|
|
80
82
|
|
|
81
83
|
/**
|
|
82
84
|
* Track which elements are currently updating attributes from property setters.
|
|
@@ -96,7 +98,7 @@ const settingAttributes = new WeakSet<HTMLElement>();
|
|
|
96
98
|
*/
|
|
97
99
|
const pendingAttributeReflections = new WeakMap<
|
|
98
100
|
HTMLElement,
|
|
99
|
-
Map<string, {value: any; type: any; converter?: AttributeConverter<any>}>
|
|
101
|
+
Map<string, { value: any; type: any; converter?: AttributeConverter<any> }>
|
|
100
102
|
>();
|
|
101
103
|
|
|
102
104
|
/**
|
|
@@ -109,7 +111,7 @@ const pendingAttributeReflections = new WeakMap<
|
|
|
109
111
|
*/
|
|
110
112
|
const pendingUpdates = new WeakMap<
|
|
111
113
|
HTMLElement,
|
|
112
|
-
Array<{propertyKey: string; oldValue: any; newValue: any}>
|
|
114
|
+
Array<{ propertyKey: string; oldValue: any; newValue: any }>
|
|
113
115
|
>();
|
|
114
116
|
|
|
115
117
|
/**
|
|
@@ -252,10 +254,10 @@ function normalizeType(
|
|
|
252
254
|
case "array":
|
|
253
255
|
return Array;
|
|
254
256
|
default:
|
|
255
|
-
return
|
|
257
|
+
return null;
|
|
256
258
|
}
|
|
257
259
|
}
|
|
258
|
-
return type
|
|
260
|
+
return type;
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
/**
|
|
@@ -397,6 +399,19 @@ function updateAttribute(
|
|
|
397
399
|
}
|
|
398
400
|
}
|
|
399
401
|
|
|
402
|
+
const alreadLogedNodeNames = new Set<string>();
|
|
403
|
+
|
|
404
|
+
function logConnectedCallback(nodeName: string) {
|
|
405
|
+
if (!alreadLogedNodeNames.has(nodeName)) {
|
|
406
|
+
alreadLogedNodeNames.add(nodeName);
|
|
407
|
+
if (typeof console !== "undefined" && console.info) {
|
|
408
|
+
console.info(
|
|
409
|
+
`[CustomElement] connectedCallback called for <${nodeName.toLocaleLowerCase()}>`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
400
415
|
/**
|
|
401
416
|
* Class field decorator that creates a reactive property with automatic attribute syncing.
|
|
402
417
|
*
|
|
@@ -468,7 +483,13 @@ function updateAttribute(
|
|
|
468
483
|
export function property(
|
|
469
484
|
options: PropertyOptions = {},
|
|
470
485
|
): (target: any, propertyKey: string) => void {
|
|
471
|
-
const {
|
|
486
|
+
const {
|
|
487
|
+
type: rawType,
|
|
488
|
+
attribute = true,
|
|
489
|
+
reflect = false,
|
|
490
|
+
converter,
|
|
491
|
+
readonly = false,
|
|
492
|
+
} = options;
|
|
472
493
|
|
|
473
494
|
const type = normalizeType(rawType);
|
|
474
495
|
|
|
@@ -477,6 +498,132 @@ export function property(
|
|
|
477
498
|
const instanceMap = new WeakMap<any, any>();
|
|
478
499
|
const attributeName = getAttributeName(propertyKey, attribute);
|
|
479
500
|
|
|
501
|
+
// --- Patch lifecycle methods ONLY if defined in derived class ---
|
|
502
|
+
// Only patch once per class
|
|
503
|
+
if (!target.__p_elements_core_lifecycle_patch_applied) {
|
|
504
|
+
// Patch connectedCallback
|
|
505
|
+
if (
|
|
506
|
+
Object.prototype.hasOwnProperty.call(target, "connectedCallback") &&
|
|
507
|
+
typeof target.connectedCallback === "function"
|
|
508
|
+
) {
|
|
509
|
+
const originalConnected = target.connectedCallback;
|
|
510
|
+
target.connectedCallback = function patchedConnectedCallback(
|
|
511
|
+
...args: any[]
|
|
512
|
+
) {
|
|
513
|
+
const PATCHED_FLAG = "__p_elements_core_pending_updates_called";
|
|
514
|
+
if (!this[PATCHED_FLAG]) {
|
|
515
|
+
let superCalled = false;
|
|
516
|
+
const origProcess = processPendingUpdates;
|
|
517
|
+
const self = this;
|
|
518
|
+
function wrappedProcessPendingUpdates(element: any) {
|
|
519
|
+
if (element === self) {
|
|
520
|
+
superCalled = true;
|
|
521
|
+
}
|
|
522
|
+
return origProcess(element);
|
|
523
|
+
}
|
|
524
|
+
(globalThis as any).__origProcessPendingUpdates =
|
|
525
|
+
processPendingUpdates;
|
|
526
|
+
(globalThis as any).processPendingUpdates =
|
|
527
|
+
wrappedProcessPendingUpdates;
|
|
528
|
+
try {
|
|
529
|
+
if (originalConnected) {
|
|
530
|
+
originalConnected.apply(this, args);
|
|
531
|
+
}
|
|
532
|
+
} finally {
|
|
533
|
+
(globalThis as any).processPendingUpdates = (
|
|
534
|
+
globalThis as any
|
|
535
|
+
).__origProcessPendingUpdates;
|
|
536
|
+
delete (globalThis as any).__origProcessPendingUpdates;
|
|
537
|
+
}
|
|
538
|
+
if (!superCalled) {
|
|
539
|
+
let baseProto = Object.getPrototypeOf(target);
|
|
540
|
+
let baseConnected = baseProto && baseProto.connectedCallback;
|
|
541
|
+
if (typeof baseConnected === "function") {
|
|
542
|
+
baseConnected.apply(this, args);
|
|
543
|
+
if (typeof console !== "undefined" && console.info) {
|
|
544
|
+
// log this once per nodeName
|
|
545
|
+
logConnectedCallback(this.nodeName);
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
processPendingUpdates(this);
|
|
549
|
+
if (typeof console !== "undefined" && console.info) {
|
|
550
|
+
console.info(
|
|
551
|
+
`[p-elements-core] called processPendingUpdates automatically for <${this.nodeName}>`,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
this[PATCHED_FLAG] = true;
|
|
557
|
+
} else if (originalConnected) {
|
|
558
|
+
originalConnected.apply(this, args);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Patch disconnectedCallback
|
|
564
|
+
if (
|
|
565
|
+
Object.prototype.hasOwnProperty.call(target, "disconnectedCallback") &&
|
|
566
|
+
typeof target.disconnectedCallback === "function"
|
|
567
|
+
) {
|
|
568
|
+
const originalDisconnected = target.disconnectedCallback;
|
|
569
|
+
target.disconnectedCallback = function patchedDisconnectedCallback(
|
|
570
|
+
...args: any[]
|
|
571
|
+
) {
|
|
572
|
+
const PATCHED_FLAG = "__p_elements_core_disconnected_called";
|
|
573
|
+
if (!this[PATCHED_FLAG]) {
|
|
574
|
+
let superCalled = false;
|
|
575
|
+
const baseProto = Object.getPrototypeOf(target);
|
|
576
|
+
const baseDisconnected =
|
|
577
|
+
baseProto && baseProto.disconnectedCallback;
|
|
578
|
+
if (originalDisconnected) {
|
|
579
|
+
originalDisconnected.apply(this, args);
|
|
580
|
+
superCalled = true;
|
|
581
|
+
}
|
|
582
|
+
if (!superCalled && typeof baseDisconnected === "function") {
|
|
583
|
+
baseDisconnected.apply(this, args);
|
|
584
|
+
}
|
|
585
|
+
this[PATCHED_FLAG] = true;
|
|
586
|
+
} else if (originalDisconnected) {
|
|
587
|
+
originalDisconnected.apply(this, args);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Patch attributeChangedCallback
|
|
593
|
+
if (
|
|
594
|
+
Object.prototype.hasOwnProperty.call(
|
|
595
|
+
target,
|
|
596
|
+
"attributeChangedCallback",
|
|
597
|
+
) &&
|
|
598
|
+
typeof target.attributeChangedCallback === "function"
|
|
599
|
+
) {
|
|
600
|
+
const originalAttributeChanged = target.attributeChangedCallback;
|
|
601
|
+
target.attributeChangedCallback =
|
|
602
|
+
function patchedAttributeChangedCallback(...args: any[]) {
|
|
603
|
+
const PATCHED_FLAG = "__p_elements_core_attribute_changed_called";
|
|
604
|
+
|
|
605
|
+
if (!this[PATCHED_FLAG]) {
|
|
606
|
+
let superCalled = false;
|
|
607
|
+
const baseProto = Object.getPrototypeOf(target);
|
|
608
|
+
const baseAttributeChanged =
|
|
609
|
+
baseProto && baseProto.attributeChangedCallback;
|
|
610
|
+
if (originalAttributeChanged) {
|
|
611
|
+
originalAttributeChanged.apply(this, args);
|
|
612
|
+
superCalled = true;
|
|
613
|
+
}
|
|
614
|
+
if (!superCalled && typeof baseAttributeChanged === "function") {
|
|
615
|
+
baseAttributeChanged.apply(this, args);
|
|
616
|
+
}
|
|
617
|
+
this[PATCHED_FLAG] = true;
|
|
618
|
+
} else if (originalAttributeChanged) {
|
|
619
|
+
originalAttributeChanged.apply(this, args);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
target.__p_elements_core_lifecycle_patch_applied = true;
|
|
625
|
+
}
|
|
626
|
+
|
|
480
627
|
Object.defineProperty(target, propertyKey, {
|
|
481
628
|
get(this: any) {
|
|
482
629
|
// On first access, check if an HTML attribute exists and use that instead
|
|
@@ -520,8 +667,18 @@ export function property(
|
|
|
520
667
|
return undefined;
|
|
521
668
|
},
|
|
522
669
|
set(this: any, value: any) {
|
|
670
|
+
// If readonly is true and value already exists, prevent setting
|
|
671
|
+
if (readonly && instanceMap.has(this)) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
523
675
|
const oldValue = instanceMap.get(this);
|
|
524
676
|
let convertedValue: any;
|
|
677
|
+
let isRenderOnSet = false;
|
|
678
|
+
|
|
679
|
+
if ((type === null || type === undefined) && value !== oldValue) {
|
|
680
|
+
isRenderOnSet = true;
|
|
681
|
+
}
|
|
525
682
|
|
|
526
683
|
// On first set, check if an HTML attribute exists and use that instead of the default
|
|
527
684
|
if (
|
|
@@ -545,7 +702,6 @@ export function property(
|
|
|
545
702
|
|
|
546
703
|
// Trigger update for initial HTML attribute only if connected
|
|
547
704
|
if (this.isConnected) {
|
|
548
|
-
|
|
549
705
|
if (typeof this.renderNow === "function") {
|
|
550
706
|
this.renderNow();
|
|
551
707
|
}
|
|
@@ -561,14 +717,19 @@ export function property(
|
|
|
561
717
|
}
|
|
562
718
|
pendingUpdates
|
|
563
719
|
.get(this)!
|
|
564
|
-
.push({propertyKey, oldValue, newValue: convertedValue});
|
|
720
|
+
.push({ propertyKey, oldValue, newValue: convertedValue });
|
|
565
721
|
}
|
|
566
722
|
}
|
|
723
|
+
|
|
567
724
|
return;
|
|
568
725
|
}
|
|
569
726
|
}
|
|
727
|
+
if (isRenderOnSet) {
|
|
728
|
+
convertedValue = value;
|
|
729
|
+
} else {
|
|
730
|
+
convertedValue = convertPropertyValue(value, type, converter);
|
|
731
|
+
}
|
|
570
732
|
|
|
571
|
-
convertedValue = convertPropertyValue(value, type, converter);
|
|
572
733
|
if (oldValue === convertedValue) {
|
|
573
734
|
return;
|
|
574
735
|
}
|
|
@@ -625,9 +786,14 @@ export function property(
|
|
|
625
786
|
}
|
|
626
787
|
pendingUpdates
|
|
627
788
|
.get(this)!
|
|
628
|
-
.push({propertyKey, oldValue, newValue: convertedValue});
|
|
789
|
+
.push({ propertyKey, oldValue, newValue: convertedValue });
|
|
629
790
|
}
|
|
630
791
|
}
|
|
792
|
+
|
|
793
|
+
// Call renderNow if @RenderOnSet is used and value changed
|
|
794
|
+
if (isRenderOnSet && typeof this.renderNow === "function") {
|
|
795
|
+
this.renderNow();
|
|
796
|
+
}
|
|
631
797
|
},
|
|
632
798
|
configurable: true,
|
|
633
799
|
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for @query decorator
|
|
3
|
+
* Covers Shadow DOM and Light DOM queries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import '../test-setup.js';
|
|
8
|
+
import { query } from './query.js';
|
|
9
|
+
import { CustomElement } from '../custom-element.js';
|
|
10
|
+
import { customElementConfig } from './custom-element-config.js';
|
|
11
|
+
import { generateUniqueTagName } from '../test-setup.js';
|
|
12
|
+
import { waitForRender } from '../test-utils.js';
|
|
13
|
+
|
|
14
|
+
describe('@query decorator', () => {
|
|
15
|
+
it('should query element in shadow DOM by default', async () => {
|
|
16
|
+
const tagName = generateUniqueTagName('query-shadow');
|
|
17
|
+
|
|
18
|
+
@customElementConfig({ tagName })
|
|
19
|
+
class QueryTest extends CustomElement {
|
|
20
|
+
static style = ':host { display: block; }';
|
|
21
|
+
@query('.test-target')
|
|
22
|
+
target: HTMLElement;
|
|
23
|
+
|
|
24
|
+
render() {
|
|
25
|
+
return {
|
|
26
|
+
vnodeSelector: 'div',
|
|
27
|
+
properties: {},
|
|
28
|
+
children: [
|
|
29
|
+
{
|
|
30
|
+
vnodeSelector: 'span',
|
|
31
|
+
properties: { class: 'test-target' },
|
|
32
|
+
text: 'Found me!',
|
|
33
|
+
domNode: null as any,
|
|
34
|
+
children: []
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
text: undefined,
|
|
38
|
+
domNode: null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const el = document.createElement(tagName) as QueryTest;
|
|
44
|
+
document.body.appendChild(el);
|
|
45
|
+
await waitForRender(el);
|
|
46
|
+
|
|
47
|
+
expect(el.target).toBeDefined();
|
|
48
|
+
expect(el.target.tagName).toBe('SPAN');
|
|
49
|
+
expect(el.target.classList.contains('test-target')).toBe(true);
|
|
50
|
+
|
|
51
|
+
document.body.removeChild(el);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should query element in light DOM when useShadowRoot is false', async () => {
|
|
55
|
+
const tagName = generateUniqueTagName('query-light');
|
|
56
|
+
|
|
57
|
+
@customElementConfig({ tagName })
|
|
58
|
+
class QueryTest extends CustomElement {
|
|
59
|
+
static style = ':host { display: block; }';
|
|
60
|
+
@query('.light-target', false)
|
|
61
|
+
target: HTMLElement;
|
|
62
|
+
|
|
63
|
+
render() {
|
|
64
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const el = document.createElement(tagName) as QueryTest;
|
|
69
|
+
const lightChild = document.createElement('div');
|
|
70
|
+
lightChild.className = 'light-target';
|
|
71
|
+
lightChild.textContent = 'Light DOM';
|
|
72
|
+
el.appendChild(lightChild);
|
|
73
|
+
|
|
74
|
+
document.body.appendChild(el);
|
|
75
|
+
await waitForRender(el);
|
|
76
|
+
|
|
77
|
+
expect(el.target).toBeDefined();
|
|
78
|
+
expect(el.target.className).toBe('light-target');
|
|
79
|
+
|
|
80
|
+
document.body.removeChild(el);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return null when element not found', async () => {
|
|
84
|
+
const tagName = generateUniqueTagName('query-missing');
|
|
85
|
+
|
|
86
|
+
@customElementConfig({ tagName })
|
|
87
|
+
class QueryTest extends CustomElement {
|
|
88
|
+
static style = ':host { display: block; }';
|
|
89
|
+
@query('.does-not-exist')
|
|
90
|
+
target: HTMLElement;
|
|
91
|
+
|
|
92
|
+
render() {
|
|
93
|
+
return { vnodeSelector: 'div', properties: {}, children: [], text: undefined, domNode: null };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const el = document.createElement(tagName) as QueryTest;
|
|
98
|
+
document.body.appendChild(el);
|
|
99
|
+
await waitForRender(el);
|
|
100
|
+
|
|
101
|
+
expect(el.target).toBeNull();
|
|
102
|
+
|
|
103
|
+
document.body.removeChild(el);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should update query result when DOM changes', async () => {
|
|
107
|
+
const tagName = generateUniqueTagName('query-dynamic');
|
|
108
|
+
let showElement = false;
|
|
109
|
+
|
|
110
|
+
@customElementConfig({ tagName })
|
|
111
|
+
class QueryTest extends CustomElement {
|
|
112
|
+
static style = ':host { display: block; }';
|
|
113
|
+
@query('.dynamic')
|
|
114
|
+
target: HTMLElement;
|
|
115
|
+
|
|
116
|
+
render() {
|
|
117
|
+
const children = showElement
|
|
118
|
+
? [{ vnodeSelector: 'span', properties: { class: 'dynamic' }, text: 'Dynamic', domNode: null as any, children: [] }]
|
|
119
|
+
: [];
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
vnodeSelector: 'div',
|
|
123
|
+
properties: {},
|
|
124
|
+
children,
|
|
125
|
+
text: undefined,
|
|
126
|
+
domNode: null
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const el = document.createElement(tagName) as QueryTest;
|
|
132
|
+
document.body.appendChild(el);
|
|
133
|
+
await waitForRender(el);
|
|
134
|
+
|
|
135
|
+
expect(el.target).toBeNull();
|
|
136
|
+
|
|
137
|
+
showElement = true;
|
|
138
|
+
el.renderNow();
|
|
139
|
+
await waitForRender(el);
|
|
140
|
+
|
|
141
|
+
expect(el.target).toBeDefined();
|
|
142
|
+
expect(el.target.tagName).toBe('SPAN');
|
|
143
|
+
|
|
144
|
+
document.body.removeChild(el);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CSS helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { cssApplyToCssVars, cssApplyVars, replaceApplyToCssVars } from './css.js';
|
|
7
|
+
|
|
8
|
+
describe('CSS helpers', () => {
|
|
9
|
+
describe('cssApplyToCssVars', () => {
|
|
10
|
+
it('should convert CSS @apply mixins to CSS variables', () => {
|
|
11
|
+
const inputCSS = `
|
|
12
|
+
--my-mixin: {
|
|
13
|
+
color: red;
|
|
14
|
+
font-size: 16px;
|
|
15
|
+
};
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const result = cssApplyToCssVars(inputCSS);
|
|
19
|
+
|
|
20
|
+
expect(result).toContain('--my-mixin_-_color: red');
|
|
21
|
+
expect(result).toContain('--my-mixin_-_font-size: 16px');
|
|
22
|
+
expect(cssApplyVars.has('--my-mixin')).toBe(true);
|
|
23
|
+
expect(cssApplyVars.get('--my-mixin')).toContain('color');
|
|
24
|
+
expect(cssApplyVars.get('--my-mixin')).toContain('font-size');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return null if no mixins found', () => {
|
|
28
|
+
const inputCSS = `
|
|
29
|
+
.class {
|
|
30
|
+
color: blue;
|
|
31
|
+
padding: 10px;
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const result = cssApplyToCssVars(inputCSS);
|
|
36
|
+
expect(result).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle multiple mixins', () => {
|
|
40
|
+
const inputCSS = `
|
|
41
|
+
--mixin-one: {
|
|
42
|
+
color: red;
|
|
43
|
+
};
|
|
44
|
+
--mixin-two: {
|
|
45
|
+
background: blue;
|
|
46
|
+
margin: 5px;
|
|
47
|
+
};
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const result = cssApplyToCssVars(inputCSS);
|
|
51
|
+
|
|
52
|
+
expect(result).toContain('--mixin-one_-_color: red');
|
|
53
|
+
expect(result).toContain('--mixin-two_-_background: blue');
|
|
54
|
+
expect(result).toContain('--mixin-two_-_margin: 5px');
|
|
55
|
+
expect(cssApplyVars.has('--mixin-one')).toBe(true);
|
|
56
|
+
expect(cssApplyVars.has('--mixin-two')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should preserve regular CSS variables', () => {
|
|
60
|
+
const inputCSS = `
|
|
61
|
+
--regular-var: 20px;
|
|
62
|
+
--my-mixin: {
|
|
63
|
+
padding: 10px;
|
|
64
|
+
};
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const result = cssApplyToCssVars(inputCSS);
|
|
68
|
+
|
|
69
|
+
expect(result).toContain('--regular-var: 20px');
|
|
70
|
+
expect(result).toContain('--my-mixin_-_padding: 10px');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle empty mixin values', () => {
|
|
74
|
+
const inputCSS = `
|
|
75
|
+
--empty-mixin: {
|
|
76
|
+
;
|
|
77
|
+
};
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const result = cssApplyToCssVars(inputCSS);
|
|
81
|
+
expect(result).not.toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('replaceApplyToCssVars', () => {
|
|
86
|
+
it('should replace @apply with CSS variable references', () => {
|
|
87
|
+
// First set up a mixin
|
|
88
|
+
cssApplyVars.set('--theme-colors', ['color', 'background']);
|
|
89
|
+
|
|
90
|
+
const inputCSS = `
|
|
91
|
+
.element {
|
|
92
|
+
@apply --theme-colors;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
const result = replaceApplyToCssVars(inputCSS);
|
|
97
|
+
|
|
98
|
+
expect(result).toContain('color: var(--theme-colors_-_color)');
|
|
99
|
+
expect(result).toContain('background: var(--theme-colors_-_background)');
|
|
100
|
+
expect(result).not.toContain('@apply');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle @apply with parentheses', () => {
|
|
104
|
+
cssApplyVars.set('--button-style', ['padding', 'border']);
|
|
105
|
+
|
|
106
|
+
const inputCSS = `
|
|
107
|
+
button {
|
|
108
|
+
@apply(--button-style);
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const result = replaceApplyToCssVars(inputCSS);
|
|
113
|
+
|
|
114
|
+
expect(result).toContain('padding: var(--button-style_-_padding)');
|
|
115
|
+
expect(result).toContain('border: var(--button-style_-_border)');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should leave CSS unchanged if mixin not defined', () => {
|
|
119
|
+
const inputCSS = `
|
|
120
|
+
.element {
|
|
121
|
+
@apply --undefined-mixin;
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
const result = replaceApplyToCssVars(inputCSS);
|
|
126
|
+
|
|
127
|
+
// Should still contain @apply since mixin isn't defined
|
|
128
|
+
expect(result).toContain('@apply --undefined-mixin');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle multiple @apply calls', () => {
|
|
132
|
+
cssApplyVars.set('--mixin-a', ['color']);
|
|
133
|
+
cssApplyVars.set('--mixin-b', ['background']);
|
|
134
|
+
|
|
135
|
+
const inputCSS = `
|
|
136
|
+
.one {
|
|
137
|
+
@apply --mixin-a;
|
|
138
|
+
}
|
|
139
|
+
.two {
|
|
140
|
+
@apply --mixin-b;
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const result = replaceApplyToCssVars(inputCSS);
|
|
145
|
+
|
|
146
|
+
expect(result).toContain('color: var(--mixin-a_-_color)');
|
|
147
|
+
expect(result).toContain('background: var(--mixin-b_-_background)');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|