pulse-js-framework 1.7.13 → 1.7.16
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 +46 -0
- package/cli/help.js +583 -0
- package/cli/index.js +24 -105
- package/package.json +16 -3
- package/runtime/async.js +39 -0
- package/runtime/dom-adapter.js +663 -0
- package/runtime/dom-element.js +107 -0
- package/runtime/index.js +2 -0
- package/runtime/pulse.js +40 -0
- package/runtime/ssr-async.js +229 -0
- package/runtime/ssr-hydrator.js +310 -0
- package/runtime/ssr-serializer.js +266 -0
- package/runtime/ssr.js +463 -0
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,
|