pulse-js-framework 1.7.13 → 1.7.15
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/README.md +9 -0
- package/package.json +15 -3
- package/runtime/dom-adapter.js +663 -0
package/README.md
CHANGED
|
@@ -148,6 +148,7 @@ pulse compile <file> # Compile .pulse file
|
|
|
148
148
|
|
|
149
149
|
# Code Quality
|
|
150
150
|
pulse lint [files] # Validate .pulse files
|
|
151
|
+
pulse lint --fix # Auto-fix fixable issues
|
|
151
152
|
pulse format [files] # Format .pulse files
|
|
152
153
|
pulse analyze # Analyze bundle
|
|
153
154
|
|
|
@@ -167,6 +168,8 @@ pulse scaffold page <name> # Generate page
|
|
|
167
168
|
pulse scaffold store <name> # Generate store module
|
|
168
169
|
pulse scaffold hook <name> # Generate custom hook
|
|
169
170
|
pulse scaffold service <name> # Generate API service
|
|
171
|
+
pulse scaffold context <name> # Generate context provider
|
|
172
|
+
pulse scaffold layout <name> # Generate layout component
|
|
170
173
|
|
|
171
174
|
# Documentation
|
|
172
175
|
pulse docs --generate # Generate API docs (Markdown)
|
|
@@ -328,6 +331,12 @@ const count: Pulse<number> = pulse(0);
|
|
|
328
331
|
- [API Reference](docs/api.md) - Complete API documentation
|
|
329
332
|
- [CLI Commands](docs/cli.md) - Command line interface
|
|
330
333
|
- [Pulse DSL](docs/pulse-dsl.md) - .pulse file syntax
|
|
334
|
+
- [Accessibility](docs/accessibility.md) - A11y guide and ARIA helpers
|
|
335
|
+
- [HTTP Client](docs/http.md) - Fetch wrapper with interceptors
|
|
336
|
+
- [WebSocket](docs/websocket.md) - Real-time with auto-reconnect
|
|
337
|
+
- [GraphQL](docs/graphql.md) - Queries, mutations, subscriptions
|
|
338
|
+
- [Context API](docs/context.md) - Dependency injection
|
|
339
|
+
- [DevTools](docs/devtools.md) - Debugging and profiling
|
|
331
340
|
- [Mobile Apps](docs/mobile.md) - Native Android & iOS
|
|
332
341
|
|
|
333
342
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.15",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"./compiler/lexer": "./compiler/lexer.js",
|
|
89
89
|
"./compiler/parser": "./compiler/parser.js",
|
|
90
90
|
"./compiler/transformer": "./compiler/transformer.js",
|
|
91
|
-
"./core/errors": "./
|
|
91
|
+
"./core/errors": "./runtime/errors.js",
|
|
92
92
|
"./vite": {
|
|
93
93
|
"types": "./types/index.d.ts",
|
|
94
94
|
"default": "./loader/vite-plugin.js"
|
|
@@ -109,14 +109,17 @@
|
|
|
109
109
|
"LICENSE"
|
|
110
110
|
],
|
|
111
111
|
"scripts": {
|
|
112
|
-
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:cli && npm run test:cli-ui && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:logger && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build",
|
|
112
|
+
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-element && npm run test:dom-adapter && npm run test:enhanced-mock-adapter && npm run test:router && npm run test:store && npm run test:context && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:cli && npm run test:cli-ui && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:docs-nav && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:a11y-enhanced && npm run test:logger && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build && npm run test:integration && npm run test:context-stress && npm run test:form-edge-cases && npm run test:graphql-subscriptions && npm run test:http-edge-cases && npm run test:integration-advanced && npm run test:websocket-stress",
|
|
113
113
|
"test:compiler": "node test/compiler.test.js",
|
|
114
114
|
"test:sourcemap": "node test/sourcemap.test.js",
|
|
115
115
|
"test:pulse": "node test/pulse.test.js",
|
|
116
116
|
"test:dom": "node test/dom.test.js",
|
|
117
|
+
"test:dom-element": "node test/dom-element.test.js",
|
|
117
118
|
"test:dom-adapter": "node test/dom-adapter.test.js",
|
|
119
|
+
"test:enhanced-mock-adapter": "node test/enhanced-mock-adapter.test.js",
|
|
118
120
|
"test:router": "node test/router.test.js",
|
|
119
121
|
"test:store": "node test/store.test.js",
|
|
122
|
+
"test:context": "node test/context.test.js",
|
|
120
123
|
"test:hmr": "node test/hmr.test.js",
|
|
121
124
|
"test:lint": "node test/lint.test.js",
|
|
122
125
|
"test:format": "node test/format.test.js",
|
|
@@ -126,12 +129,14 @@
|
|
|
126
129
|
"test:lru-cache": "node test/lru-cache.test.js",
|
|
127
130
|
"test:utils": "node test/utils.test.js",
|
|
128
131
|
"test:docs": "node test/docs.test.js",
|
|
132
|
+
"test:docs-nav": "node test/docs-navigation.test.js",
|
|
129
133
|
"test:async": "node test/async.test.js",
|
|
130
134
|
"test:form": "node test/form.test.js",
|
|
131
135
|
"test:http": "node test/http.test.js",
|
|
132
136
|
"test:devtools": "node test/devtools.test.js",
|
|
133
137
|
"test:native": "node test/native.test.js",
|
|
134
138
|
"test:a11y": "node test/a11y.test.js",
|
|
139
|
+
"test:a11y-enhanced": "node test/a11y-enhanced.test.js",
|
|
135
140
|
"test:logger": "node test/logger.test.js",
|
|
136
141
|
"test:errors": "node test/errors.test.js",
|
|
137
142
|
"test:security": "node test/security.test.js",
|
|
@@ -141,6 +146,13 @@
|
|
|
141
146
|
"test:scaffold": "node test/scaffold.test.js",
|
|
142
147
|
"test:test-runner": "node test/test-runner.test.js",
|
|
143
148
|
"test:build": "node test/build.test.js",
|
|
149
|
+
"test:integration": "node test/integration.test.js",
|
|
150
|
+
"test:context-stress": "node test/context-stress.test.js",
|
|
151
|
+
"test:form-edge-cases": "node test/form-edge-cases.test.js",
|
|
152
|
+
"test:graphql-subscriptions": "node test/graphql-subscriptions.test.js",
|
|
153
|
+
"test:http-edge-cases": "node test/http-edge-cases.test.js",
|
|
154
|
+
"test:integration-advanced": "node test/integration-advanced.test.js",
|
|
155
|
+
"test:websocket-stress": "node test/websocket-stress.test.js",
|
|
144
156
|
"build:netlify": "node scripts/build-netlify.js",
|
|
145
157
|
"version": "node scripts/sync-version.js",
|
|
146
158
|
"docs": "node cli/index.js dev docs"
|
package/runtime/dom-adapter.js
CHANGED
|
@@ -901,6 +901,661 @@ export function withAdapter(adapter, fn) {
|
|
|
901
901
|
}
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
+
// ============================================================================
|
|
905
|
+
// Enhanced Mock Classes for Testing
|
|
906
|
+
// ============================================================================
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Mock Canvas 2D rendering context for color parsing in a11y tests.
|
|
910
|
+
*/
|
|
911
|
+
export class MockCanvasContext {
|
|
912
|
+
constructor() {
|
|
913
|
+
this.fillStyle = '#000000';
|
|
914
|
+
this._imageData = new Uint8ClampedArray([0, 0, 0, 255]);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
fillRect(x, y, width, height) {
|
|
918
|
+
// Parse fillStyle to RGB and store in imageData
|
|
919
|
+
const color = this._parseColor(this.fillStyle);
|
|
920
|
+
this._imageData[0] = color.r;
|
|
921
|
+
this._imageData[1] = color.g;
|
|
922
|
+
this._imageData[2] = color.b;
|
|
923
|
+
this._imageData[3] = 255;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
getImageData(x, y, width, height) {
|
|
927
|
+
return { data: this._imageData };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Parse CSS color to RGB values.
|
|
932
|
+
* Supports: hex (#fff, #ffffff), rgb(), rgba(), named colors
|
|
933
|
+
*/
|
|
934
|
+
_parseColor(color) {
|
|
935
|
+
if (!color || color === 'transparent') {
|
|
936
|
+
return { r: 0, g: 0, b: 0 };
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Hex colors
|
|
940
|
+
if (color.startsWith('#')) {
|
|
941
|
+
let hex = color.slice(1);
|
|
942
|
+
if (hex.length === 3) {
|
|
943
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
947
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
948
|
+
b: parseInt(hex.slice(4, 6), 16)
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// rgb() and rgba()
|
|
953
|
+
const rgbMatch = color.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
954
|
+
if (rgbMatch) {
|
|
955
|
+
return {
|
|
956
|
+
r: parseInt(rgbMatch[1], 10),
|
|
957
|
+
g: parseInt(rgbMatch[2], 10),
|
|
958
|
+
b: parseInt(rgbMatch[3], 10)
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Named colors (common subset)
|
|
963
|
+
const namedColors = {
|
|
964
|
+
white: { r: 255, g: 255, b: 255 },
|
|
965
|
+
black: { r: 0, g: 0, b: 0 },
|
|
966
|
+
red: { r: 255, g: 0, b: 0 },
|
|
967
|
+
green: { r: 0, g: 128, b: 0 },
|
|
968
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
969
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
970
|
+
orange: { r: 255, g: 165, b: 0 },
|
|
971
|
+
gray: { r: 128, g: 128, b: 128 },
|
|
972
|
+
grey: { r: 128, g: 128, b: 128 }
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
return namedColors[color.toLowerCase()] || { r: 0, g: 0, b: 0 };
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Mock MediaQueryList for matchMedia() testing.
|
|
981
|
+
*/
|
|
982
|
+
export class MockMediaQueryList {
|
|
983
|
+
constructor(query, matches = false) {
|
|
984
|
+
this.media = query;
|
|
985
|
+
this.matches = matches;
|
|
986
|
+
this._listeners = [];
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
addEventListener(event, listener) {
|
|
990
|
+
if (event === 'change') {
|
|
991
|
+
this._listeners.push(listener);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
removeEventListener(event, listener) {
|
|
996
|
+
if (event === 'change') {
|
|
997
|
+
const index = this._listeners.indexOf(listener);
|
|
998
|
+
if (index !== -1) {
|
|
999
|
+
this._listeners.splice(index, 1);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Simulate a media query change (for testing).
|
|
1006
|
+
*/
|
|
1007
|
+
_setMatches(matches) {
|
|
1008
|
+
if (this.matches !== matches) {
|
|
1009
|
+
this.matches = matches;
|
|
1010
|
+
const event = { matches, media: this.media };
|
|
1011
|
+
this._listeners.forEach(listener => listener(event));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Deprecated but still used in some code
|
|
1016
|
+
addListener(listener) {
|
|
1017
|
+
this.addEventListener('change', listener);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
removeListener(listener) {
|
|
1021
|
+
this.removeEventListener('change', listener);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Mock MutationObserver for DOM change tracking.
|
|
1027
|
+
*/
|
|
1028
|
+
export class MockMutationObserver {
|
|
1029
|
+
constructor(callback) {
|
|
1030
|
+
this._callback = callback;
|
|
1031
|
+
this._observing = false;
|
|
1032
|
+
this._target = null;
|
|
1033
|
+
this._options = null;
|
|
1034
|
+
this._mutations = [];
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
observe(target, options) {
|
|
1038
|
+
this._observing = true;
|
|
1039
|
+
this._target = target;
|
|
1040
|
+
this._options = options;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
disconnect() {
|
|
1044
|
+
this._observing = false;
|
|
1045
|
+
this._target = null;
|
|
1046
|
+
this._options = null;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
takeRecords() {
|
|
1050
|
+
const records = [...this._mutations];
|
|
1051
|
+
this._mutations = [];
|
|
1052
|
+
return records;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Simulate a mutation (for testing).
|
|
1057
|
+
*/
|
|
1058
|
+
_trigger(mutations) {
|
|
1059
|
+
if (this._observing && this._callback) {
|
|
1060
|
+
this._mutations.push(...mutations);
|
|
1061
|
+
this._callback(mutations, this);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Mock Performance API.
|
|
1068
|
+
*/
|
|
1069
|
+
export class MockPerformance {
|
|
1070
|
+
constructor() {
|
|
1071
|
+
this._startTime = Date.now();
|
|
1072
|
+
this._marks = new Map();
|
|
1073
|
+
this._measures = new Map();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
now() {
|
|
1077
|
+
return Date.now() - this._startTime;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
mark(name) {
|
|
1081
|
+
this._marks.set(name, this.now());
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
measure(name, startMark, endMark) {
|
|
1085
|
+
const start = this._marks.get(startMark) || 0;
|
|
1086
|
+
const end = this._marks.get(endMark) || this.now();
|
|
1087
|
+
this._measures.set(name, { name, duration: end - start, startTime: start });
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
getEntriesByName(name) {
|
|
1091
|
+
const measure = this._measures.get(name);
|
|
1092
|
+
return measure ? [measure] : [];
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
clearMarks(name) {
|
|
1096
|
+
if (name) {
|
|
1097
|
+
this._marks.delete(name);
|
|
1098
|
+
} else {
|
|
1099
|
+
this._marks.clear();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
clearMeasures(name) {
|
|
1104
|
+
if (name) {
|
|
1105
|
+
this._measures.delete(name);
|
|
1106
|
+
} else {
|
|
1107
|
+
this._measures.clear();
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Mock computed style object.
|
|
1114
|
+
*/
|
|
1115
|
+
export class MockCSSStyleDeclaration {
|
|
1116
|
+
constructor(styles = {}) {
|
|
1117
|
+
// Default visible styles
|
|
1118
|
+
this.display = styles.display || 'block';
|
|
1119
|
+
this.visibility = styles.visibility || 'visible';
|
|
1120
|
+
this.color = styles.color || 'rgb(0, 0, 0)';
|
|
1121
|
+
this.backgroundColor = styles.backgroundColor || 'rgba(0, 0, 0, 0)';
|
|
1122
|
+
this.fontSize = styles.fontSize || '16px';
|
|
1123
|
+
this.fontWeight = styles.fontWeight || '400';
|
|
1124
|
+
this.position = styles.position || 'static';
|
|
1125
|
+
this.width = styles.width || 'auto';
|
|
1126
|
+
this.height = styles.height || 'auto';
|
|
1127
|
+
|
|
1128
|
+
// Allow custom styles
|
|
1129
|
+
Object.assign(this, styles);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Mock Window object for global browser APIs.
|
|
1135
|
+
*/
|
|
1136
|
+
export class MockWindow {
|
|
1137
|
+
constructor(options = {}) {
|
|
1138
|
+
this._mediaQueryResults = options.mediaQueryResults || {};
|
|
1139
|
+
this._mediaQueryLists = new Map();
|
|
1140
|
+
|
|
1141
|
+
this.innerWidth = options.innerWidth || 1024;
|
|
1142
|
+
this.innerHeight = options.innerHeight || 768;
|
|
1143
|
+
this.location = {
|
|
1144
|
+
href: options.locationHref || 'http://localhost:3000/',
|
|
1145
|
+
pathname: options.locationPathname || '/',
|
|
1146
|
+
search: '',
|
|
1147
|
+
hash: ''
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
this.performance = new MockPerformance();
|
|
1151
|
+
this._eventListeners = new Map();
|
|
1152
|
+
this._animationFrameCallbacks = [];
|
|
1153
|
+
this._animationFrameId = 0;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
matchMedia(query) {
|
|
1157
|
+
if (!this._mediaQueryLists.has(query)) {
|
|
1158
|
+
const matches = this._evaluateMediaQuery(query);
|
|
1159
|
+
this._mediaQueryLists.set(query, new MockMediaQueryList(query, matches));
|
|
1160
|
+
}
|
|
1161
|
+
return this._mediaQueryLists.get(query);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
_evaluateMediaQuery(query) {
|
|
1165
|
+
// Check custom results first
|
|
1166
|
+
if (this._mediaQueryResults[query] !== undefined) {
|
|
1167
|
+
return this._mediaQueryResults[query];
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Evaluate common media queries
|
|
1171
|
+
if (query.includes('prefers-reduced-motion: reduce')) return false;
|
|
1172
|
+
if (query.includes('prefers-color-scheme: dark')) return false;
|
|
1173
|
+
if (query.includes('prefers-color-scheme: light')) return true;
|
|
1174
|
+
if (query.includes('prefers-contrast: more')) return false;
|
|
1175
|
+
if (query.includes('prefers-reduced-transparency: reduce')) return false;
|
|
1176
|
+
if (query.includes('forced-colors: active')) return false;
|
|
1177
|
+
|
|
1178
|
+
// Width queries
|
|
1179
|
+
const minWidthMatch = query.match(/min-width:\s*(\d+)px/);
|
|
1180
|
+
if (minWidthMatch) {
|
|
1181
|
+
return this.innerWidth >= parseInt(minWidthMatch[1], 10);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const maxWidthMatch = query.match(/max-width:\s*(\d+)px/);
|
|
1185
|
+
if (maxWidthMatch) {
|
|
1186
|
+
return this.innerWidth <= parseInt(maxWidthMatch[1], 10);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Set media query result (for testing).
|
|
1194
|
+
*/
|
|
1195
|
+
setMediaQueryResult(query, matches) {
|
|
1196
|
+
this._mediaQueryResults[query] = matches;
|
|
1197
|
+
if (this._mediaQueryLists.has(query)) {
|
|
1198
|
+
this._mediaQueryLists.get(query)._setMatches(matches);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
requestAnimationFrame(callback) {
|
|
1203
|
+
const id = ++this._animationFrameId;
|
|
1204
|
+
this._animationFrameCallbacks.push({ id, callback });
|
|
1205
|
+
return id;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
cancelAnimationFrame(id) {
|
|
1209
|
+
this._animationFrameCallbacks = this._animationFrameCallbacks.filter(c => c.id !== id);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Run all pending animation frame callbacks (for testing).
|
|
1214
|
+
*/
|
|
1215
|
+
flushAnimationFrames() {
|
|
1216
|
+
const callbacks = [...this._animationFrameCallbacks];
|
|
1217
|
+
this._animationFrameCallbacks = [];
|
|
1218
|
+
callbacks.forEach(({ callback }) => callback(this.performance.now()));
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
addEventListener(event, handler, options) {
|
|
1222
|
+
if (!this._eventListeners.has(event)) {
|
|
1223
|
+
this._eventListeners.set(event, []);
|
|
1224
|
+
}
|
|
1225
|
+
this._eventListeners.get(event).push({ handler, options });
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
removeEventListener(event, handler, options) {
|
|
1229
|
+
const listeners = this._eventListeners.get(event);
|
|
1230
|
+
if (listeners) {
|
|
1231
|
+
const index = listeners.findIndex(l => l.handler === handler);
|
|
1232
|
+
if (index !== -1) {
|
|
1233
|
+
listeners.splice(index, 1);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
dispatchEvent(event) {
|
|
1239
|
+
const listeners = this._eventListeners.get(event.type);
|
|
1240
|
+
if (listeners) {
|
|
1241
|
+
listeners.forEach(({ handler }) => handler(event));
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
getComputedStyle(element) {
|
|
1246
|
+
// Return element's stored computed styles or defaults
|
|
1247
|
+
return element._computedStyle || new MockCSSStyleDeclaration();
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Enhanced MockElement with additional browser APIs.
|
|
1253
|
+
*/
|
|
1254
|
+
export class EnhancedMockElement extends MockElement {
|
|
1255
|
+
constructor(tagName) {
|
|
1256
|
+
super(tagName);
|
|
1257
|
+
this._boundingRect = { top: 0, left: 0, width: 100, height: 50, right: 100, bottom: 50 };
|
|
1258
|
+
this._computedStyle = new MockCSSStyleDeclaration();
|
|
1259
|
+
this._canvas = null;
|
|
1260
|
+
this.hidden = false;
|
|
1261
|
+
this.inert = false;
|
|
1262
|
+
this.labels = [];
|
|
1263
|
+
this.offsetParent = {};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
getBoundingClientRect() {
|
|
1267
|
+
return { ...this._boundingRect };
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Set bounding rect (for testing).
|
|
1272
|
+
*/
|
|
1273
|
+
setBoundingRect(rect) {
|
|
1274
|
+
this._boundingRect = { ...this._boundingRect, ...rect };
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Set computed style (for testing).
|
|
1279
|
+
*/
|
|
1280
|
+
setComputedStyle(styles) {
|
|
1281
|
+
this._computedStyle = new MockCSSStyleDeclaration(styles);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
getContext(contextType) {
|
|
1285
|
+
if (contextType === '2d') {
|
|
1286
|
+
if (!this._canvas) {
|
|
1287
|
+
this._canvas = new MockCanvasContext();
|
|
1288
|
+
}
|
|
1289
|
+
return this._canvas;
|
|
1290
|
+
}
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
focus() {
|
|
1295
|
+
// Simulate focus by updating document.activeElement
|
|
1296
|
+
if (this._document) {
|
|
1297
|
+
this._document.activeElement = this;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
blur() {
|
|
1302
|
+
if (this._document && this._document.activeElement === this) {
|
|
1303
|
+
this._document.activeElement = null;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
contains(other) {
|
|
1308
|
+
if (!other) return false;
|
|
1309
|
+
if (other === this) return true;
|
|
1310
|
+
for (const child of this.childNodes) {
|
|
1311
|
+
if (child === other) return true;
|
|
1312
|
+
if (child.contains && child.contains(other)) return true;
|
|
1313
|
+
}
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
closest(selector) {
|
|
1318
|
+
// Simple implementation - check self and parents
|
|
1319
|
+
let current = this;
|
|
1320
|
+
while (current) {
|
|
1321
|
+
if (this._matchesSelector(current, selector)) {
|
|
1322
|
+
return current;
|
|
1323
|
+
}
|
|
1324
|
+
current = current.parentNode;
|
|
1325
|
+
}
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
_matchesSelector(element, selector) {
|
|
1330
|
+
if (!element.tagName) return false;
|
|
1331
|
+
|
|
1332
|
+
// Tag selector
|
|
1333
|
+
if (selector === element.tagName.toLowerCase()) return true;
|
|
1334
|
+
|
|
1335
|
+
// ID selector
|
|
1336
|
+
if (selector.startsWith('#') && element.id === selector.slice(1)) return true;
|
|
1337
|
+
|
|
1338
|
+
// Class selector
|
|
1339
|
+
if (selector.startsWith('.') && element.classList?.contains(selector.slice(1))) return true;
|
|
1340
|
+
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
querySelectorAll(selector) {
|
|
1345
|
+
const results = [];
|
|
1346
|
+
this._findAll(this, selector, results);
|
|
1347
|
+
return results;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
querySelector(selector) {
|
|
1351
|
+
const all = this.querySelectorAll(selector);
|
|
1352
|
+
return all[0] || null;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
_findAll(node, selector, results) {
|
|
1356
|
+
for (const child of node.childNodes || []) {
|
|
1357
|
+
if (this._matchesSelector(child, selector)) {
|
|
1358
|
+
results.push(child);
|
|
1359
|
+
}
|
|
1360
|
+
if (child._findAll) {
|
|
1361
|
+
child._findAll(child, selector, results);
|
|
1362
|
+
} else {
|
|
1363
|
+
this._findAll(child, selector, results);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Enhanced Mock DOM Adapter with full browser API simulation.
|
|
1371
|
+
* Provides comprehensive testing support for a11y, devtools, and other
|
|
1372
|
+
* browser-dependent modules.
|
|
1373
|
+
*
|
|
1374
|
+
* @implements {DOMAdapter}
|
|
1375
|
+
*/
|
|
1376
|
+
export class EnhancedMockAdapter extends MockDOMAdapter {
|
|
1377
|
+
constructor(options = {}) {
|
|
1378
|
+
super();
|
|
1379
|
+
|
|
1380
|
+
// Replace body with enhanced element
|
|
1381
|
+
this._body = new EnhancedMockElement('body');
|
|
1382
|
+
this._document.appendChild(this._body);
|
|
1383
|
+
|
|
1384
|
+
// Mock window with configurable options
|
|
1385
|
+
this._window = new MockWindow(options);
|
|
1386
|
+
|
|
1387
|
+
// Link document to window
|
|
1388
|
+
this._body._document = this;
|
|
1389
|
+
this.activeElement = null;
|
|
1390
|
+
|
|
1391
|
+
// Expose MutationObserver constructor
|
|
1392
|
+
this.MutationObserver = MockMutationObserver;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
createElement(tagName) {
|
|
1396
|
+
const el = new EnhancedMockElement(tagName);
|
|
1397
|
+
el._document = this;
|
|
1398
|
+
return el;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Get computed style for an element.
|
|
1403
|
+
*/
|
|
1404
|
+
getComputedStyle(element) {
|
|
1405
|
+
return this._window.getComputedStyle(element);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Get the mock window object.
|
|
1410
|
+
*/
|
|
1411
|
+
getWindow() {
|
|
1412
|
+
return this._window;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Request animation frame.
|
|
1417
|
+
*/
|
|
1418
|
+
requestAnimationFrame(callback) {
|
|
1419
|
+
return this._window.requestAnimationFrame(callback);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Cancel animation frame.
|
|
1424
|
+
*/
|
|
1425
|
+
cancelAnimationFrame(id) {
|
|
1426
|
+
this._window.cancelAnimationFrame(id);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Get performance API.
|
|
1431
|
+
*/
|
|
1432
|
+
getPerformance() {
|
|
1433
|
+
return this._window.performance;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Match media query.
|
|
1438
|
+
*/
|
|
1439
|
+
matchMedia(query) {
|
|
1440
|
+
return this._window.matchMedia(query);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Create a MutationObserver.
|
|
1445
|
+
*/
|
|
1446
|
+
createMutationObserver(callback) {
|
|
1447
|
+
return new MockMutationObserver(callback);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Get document element (html).
|
|
1452
|
+
*/
|
|
1453
|
+
getDocumentElement() {
|
|
1454
|
+
return this._document;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Get active element.
|
|
1459
|
+
*/
|
|
1460
|
+
getActiveElement() {
|
|
1461
|
+
return this.activeElement;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Set active element (for testing).
|
|
1466
|
+
*/
|
|
1467
|
+
setActiveElement(element) {
|
|
1468
|
+
this.activeElement = element;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Get element by ID.
|
|
1473
|
+
*/
|
|
1474
|
+
getElementById(id) {
|
|
1475
|
+
return this._findById(this._body, id);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Test helpers
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* Set media query result (for testing user preferences).
|
|
1482
|
+
* @param {string} query - Media query string
|
|
1483
|
+
* @param {boolean} matches - Whether the query matches
|
|
1484
|
+
*/
|
|
1485
|
+
setMediaQueryResult(query, matches) {
|
|
1486
|
+
this._window.setMediaQueryResult(query, matches);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* Run all pending animation frames (for testing).
|
|
1491
|
+
*/
|
|
1492
|
+
flushAnimationFrames() {
|
|
1493
|
+
this._window.flushAnimationFrames();
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Reset the mock DOM state.
|
|
1498
|
+
*/
|
|
1499
|
+
reset() {
|
|
1500
|
+
super.reset();
|
|
1501
|
+
this._body = new EnhancedMockElement('body');
|
|
1502
|
+
this._body._document = this;
|
|
1503
|
+
this._document.childNodes = [];
|
|
1504
|
+
this._document.appendChild(this._body);
|
|
1505
|
+
this.activeElement = null;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Install global mocks for browser testing.
|
|
1510
|
+
* Installs mocks on globalThis for modules that directly access browser APIs.
|
|
1511
|
+
* @returns {Function} Cleanup function to restore original globals
|
|
1512
|
+
*/
|
|
1513
|
+
installGlobalMocks() {
|
|
1514
|
+
const originals = {
|
|
1515
|
+
document: globalThis.document,
|
|
1516
|
+
window: globalThis.window,
|
|
1517
|
+
getComputedStyle: globalThis.getComputedStyle,
|
|
1518
|
+
requestAnimationFrame: globalThis.requestAnimationFrame,
|
|
1519
|
+
cancelAnimationFrame: globalThis.cancelAnimationFrame,
|
|
1520
|
+
MutationObserver: globalThis.MutationObserver,
|
|
1521
|
+
performance: globalThis.performance
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
// Create mock document
|
|
1525
|
+
globalThis.document = {
|
|
1526
|
+
body: this._body,
|
|
1527
|
+
documentElement: this._document,
|
|
1528
|
+
activeElement: null,
|
|
1529
|
+
createElement: (tag) => this.createElement(tag),
|
|
1530
|
+
createTextNode: (text) => this.createTextNode(text),
|
|
1531
|
+
createComment: (data) => this.createComment(data),
|
|
1532
|
+
createDocumentFragment: () => this.createDocumentFragment(),
|
|
1533
|
+
querySelector: (sel) => this.querySelector(sel),
|
|
1534
|
+
querySelectorAll: (sel) => this._body.querySelectorAll(sel),
|
|
1535
|
+
getElementById: (id) => this.getElementById(id),
|
|
1536
|
+
addEventListener: (e, h, o) => this._window.addEventListener(e, h, o),
|
|
1537
|
+
removeEventListener: (e, h, o) => this._window.removeEventListener(e, h, o)
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
globalThis.window = this._window;
|
|
1541
|
+
globalThis.getComputedStyle = (el) => this.getComputedStyle(el);
|
|
1542
|
+
globalThis.requestAnimationFrame = (cb) => this.requestAnimationFrame(cb);
|
|
1543
|
+
globalThis.cancelAnimationFrame = (id) => this.cancelAnimationFrame(id);
|
|
1544
|
+
globalThis.MutationObserver = MockMutationObserver;
|
|
1545
|
+
globalThis.performance = this._window.performance;
|
|
1546
|
+
|
|
1547
|
+
return () => {
|
|
1548
|
+
globalThis.document = originals.document;
|
|
1549
|
+
globalThis.window = originals.window;
|
|
1550
|
+
globalThis.getComputedStyle = originals.getComputedStyle;
|
|
1551
|
+
globalThis.requestAnimationFrame = originals.requestAnimationFrame;
|
|
1552
|
+
globalThis.cancelAnimationFrame = originals.cancelAnimationFrame;
|
|
1553
|
+
globalThis.MutationObserver = originals.MutationObserver;
|
|
1554
|
+
globalThis.performance = originals.performance;
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
904
1559
|
// ============================================================================
|
|
905
1560
|
// Exports
|
|
906
1561
|
// ============================================================================
|
|
@@ -908,11 +1563,19 @@ export function withAdapter(adapter, fn) {
|
|
|
908
1563
|
export default {
|
|
909
1564
|
BrowserDOMAdapter,
|
|
910
1565
|
MockDOMAdapter,
|
|
1566
|
+
EnhancedMockAdapter,
|
|
911
1567
|
MockNode,
|
|
912
1568
|
MockElement,
|
|
1569
|
+
EnhancedMockElement,
|
|
913
1570
|
MockTextNode,
|
|
914
1571
|
MockCommentNode,
|
|
915
1572
|
MockDocumentFragment,
|
|
1573
|
+
MockCanvasContext,
|
|
1574
|
+
MockMediaQueryList,
|
|
1575
|
+
MockMutationObserver,
|
|
1576
|
+
MockPerformance,
|
|
1577
|
+
MockCSSStyleDeclaration,
|
|
1578
|
+
MockWindow,
|
|
916
1579
|
getAdapter,
|
|
917
1580
|
setAdapter,
|
|
918
1581
|
resetAdapter,
|