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.
@@ -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,