@node-projects/web-component-designer 0.1.332 → 0.1.334

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/jest.config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export default {
2
2
  roots: ['<rootDir>/tests'],
3
- testMatch: ['**/?(*.)+(spec|test).+(ts|tsx|js)'],
4
- preset: "ts-jest",
3
+ testMatch: ['**/?(*.)+(spec|test).+(mts|ts|tsx|mjs|js)'],
4
+ preset: "ts-jest/presets/default-esm",
5
5
  testEnvironment: "node",
6
6
  extensionsToTreatAsEsm: ['.ts', '.mts'],
7
7
  transform: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "description": "A WYSIWYG designer webcomponent for html components",
3
3
  "name": "@node-projects/web-component-designer",
4
- "version": "0.1.332",
4
+ "version": "0.1.334",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "author": "jochen.kuehner@gmx.de",
@@ -13,7 +13,7 @@
13
13
  "link": "npm link",
14
14
  "watch": "pm2 start tsc --watch",
15
15
  "prepublishOnly": "npm run build && npm run bundle",
16
- "bundle": "esbuild ./dist/index-all.js --format=esm --minify --external:@node-projects/base-custom-webcomponent --platform=neutral --bundle --outfile=./dist/index-min.js"
16
+ "bundle": "esbuild ./dist/index-all.js --format=esm --minify --sourcemap --external:@node-projects/base-custom-webcomponent --platform=neutral --bundle --outfile=./dist/index-min.js"
17
17
  },
18
18
  "dependencies": {
19
19
  "@node-projects/base-custom-webcomponent": ">=0.27.8"
@@ -69,4 +69,557 @@ test('test 10', () => {
69
69
  expect(res.A).toBe(1);
70
70
  expect(res.B).toBe(3);
71
71
  expect(res.C).toBe(1);
72
- });
72
+ });
73
+
74
+ test('test 11', () => {
75
+ const res = calculateSpecificity('button:not(:nth-child(2))');
76
+ expect(res.A).toBe(0);
77
+ expect(res.B).toBe(1);
78
+ expect(res.C).toBe(1);
79
+ });
80
+
81
+ test('test 12', () => {
82
+ const res = calculateSpecificity(':nth-child(2 of .a, #b)');
83
+ expect(res.A).toBe(1);
84
+ expect(res.B).toBe(1);
85
+ expect(res.C).toBe(0);
86
+ });
87
+
88
+ test('test 13', () => {
89
+ const res = calculateSpecificity(':where(#id, .class)');
90
+ expect(res.A).toBe(0);
91
+ expect(res.B).toBe(0);
92
+ expect(res.C).toBe(0);
93
+ });
94
+
95
+ test('test 14 - escaped class', () => {
96
+ const res = calculateSpecificity('.\\31 23');
97
+ expect(res.A).toBe(0);
98
+ expect(res.B).toBe(1);
99
+ expect(res.C).toBe(0);
100
+ });
101
+
102
+ test('test 15 - escaped id', () => {
103
+ const res = calculateSpecificity('#\\#id');
104
+ expect(res.A).toBe(1);
105
+ expect(res.B).toBe(0);
106
+ expect(res.C).toBe(0);
107
+ });
108
+
109
+ test('test 16 - attribute with closing bracket in string', () => {
110
+ const res = calculateSpecificity('[data="a]b"]');
111
+ expect(res.A).toBe(0);
112
+ expect(res.B).toBe(1);
113
+ expect(res.C).toBe(0);
114
+ });
115
+
116
+ test('test 17 - attribute with parentheses in string', () => {
117
+ const res = calculateSpecificity('[data="(test)"]');
118
+ expect(res.A).toBe(0);
119
+ expect(res.B).toBe(1);
120
+ expect(res.C).toBe(0);
121
+ });
122
+
123
+ test('test 18 - nth-child with of class', () => {
124
+ const res = calculateSpecificity(':nth-child(2 of .a)');
125
+ expect(res.A).toBe(0);
126
+ expect(res.B).toBe(2); // nth-child + .a
127
+ expect(res.C).toBe(0);
128
+ });
129
+
130
+ test('test 19 - nth-child with of id', () => {
131
+ const res = calculateSpecificity(':nth-child(2 of #a)');
132
+ expect(res.A).toBe(1);
133
+ expect(res.B).toBe(1);
134
+ expect(res.C).toBe(0);
135
+ });
136
+
137
+ test('test 20 - nth-child with selector list', () => {
138
+ const res = calculateSpecificity(':nth-child(2 of .a, #b)');
139
+ expect(res.A).toBe(1); // max(#b)
140
+ expect(res.B).toBe(1); // nth-child
141
+ expect(res.C).toBe(0);
142
+ });
143
+
144
+ test('test 21 - nth-child with whitespace', () => {
145
+ const res = calculateSpecificity(':nth-child(2 of .a)');
146
+ expect(res.A).toBe(0);
147
+ expect(res.B).toBe(2);
148
+ expect(res.C).toBe(0);
149
+ });
150
+
151
+ test('test 22 - nth-child without of', () => {
152
+ const res = calculateSpecificity(':nth-child(2)');
153
+ expect(res.A).toBe(0);
154
+ expect(res.B).toBe(1);
155
+ expect(res.C).toBe(0);
156
+ });
157
+
158
+ test('test 23 - nested strings and brackets', () => {
159
+ const res = calculateSpecificity('[data="a(b[c]d)e"]');
160
+ expect(res.A).toBe(0);
161
+ expect(res.B).toBe(1);
162
+ expect(res.C).toBe(0);
163
+ });
164
+
165
+ test('test 24 - namespaced element', () => {
166
+ const res = calculateSpecificity('svg|rect');
167
+ expect(res.A).toBe(0);
168
+ expect(res.B).toBe(0);
169
+ expect(res.C).toBe(1);
170
+ });
171
+
172
+ test('test 25 - universal with namespace', () => {
173
+ const res = calculateSpecificity('*|div');
174
+ expect(res.A).toBe(0);
175
+ expect(res.B).toBe(0);
176
+ expect(res.C).toBe(1);
177
+ });
178
+
179
+ test('test 26 - empty namespace', () => {
180
+ const res = calculateSpecificity('|div');
181
+ expect(res.A).toBe(0);
182
+ expect(res.B).toBe(0);
183
+ expect(res.C).toBe(1);
184
+ });
185
+
186
+ test('test 27 - pseudo-element with args ::part', () => {
187
+ const res = calculateSpecificity('::part(button)');
188
+ expect(res.A).toBe(0);
189
+ expect(res.B).toBe(0);
190
+ expect(res.C).toBe(1);
191
+ });
192
+
193
+ test('test 28 - pseudo-element with args ::slotted', () => {
194
+ const res = calculateSpecificity('::slotted(span)');
195
+ expect(res.A).toBe(0);
196
+ expect(res.B).toBe(0);
197
+ expect(res.C).toBe(1);
198
+ });
199
+
200
+ test('test 29 - combined namespace + pseudo-element', () => {
201
+ const res = calculateSpecificity('svg|rect::part(foo)');
202
+ expect(res.A).toBe(0);
203
+ expect(res.B).toBe(0);
204
+ expect(res.C).toBe(2); // rect + ::part
205
+ });
206
+
207
+ test('test 30 - multiple pseudo-elements', () => {
208
+ const res = calculateSpecificity('::slotted(span)::part(button)');
209
+ expect(res.A).toBe(0);
210
+ expect(res.B).toBe(0);
211
+ expect(res.C).toBe(2);
212
+ });
213
+
214
+ test('test 31 - multiple pseudo-classes', () => {
215
+ const res = calculateSpecificity('div.class1.class2:hover:focus');
216
+ expect(res.A).toBe(0); // no ID
217
+ expect(res.B).toBe(4); // 2 classes + :hover + :focus
218
+ expect(res.C).toBe(1); // div type selector
219
+ });
220
+
221
+ test('test 32 - pseudo-element and class', () => {
222
+ const res = calculateSpecificity('p::before.highlight');
223
+ expect(res.A).toBe(0);
224
+ expect(res.B).toBe(1); // .highlight
225
+ expect(res.C).toBe(2); // p + ::before
226
+ });
227
+
228
+ test('test 33 - nested :is() and :not()', () => {
229
+ const res = calculateSpecificity(':is(.a, #b):not(.c)');
230
+ expect(res.A).toBe(1); // max of #b
231
+ expect(res.B).toBe(1); // .c
232
+ expect(res.C).toBe(0);
233
+ });
234
+
235
+ test('test 34 - complex descendant combinators', () => {
236
+ const res = calculateSpecificity('ul li .item > a#link:hover');
237
+ expect(res.A).toBe(1); // #link
238
+ expect(res.B).toBe(2); // .item + :hover
239
+ expect(res.C).toBe(3); // ul + li + a
240
+ });
241
+
242
+ test('test 35 - attribute selectors', () => {
243
+ const res = calculateSpecificity('[data-id="123"].active');
244
+ expect(res.A).toBe(0);
245
+ expect(res.B).toBe(2); // [attr] + .active
246
+ expect(res.C).toBe(0);
247
+ });
248
+
249
+ test('test 36 - multiple pseudo-elements', () => {
250
+ const res = calculateSpecificity('div::first-line::after');
251
+ expect(res.A).toBe(0);
252
+ expect(res.B).toBe(0);
253
+ expect(res.C).toBe(3); // div + ::first-line + ::after
254
+ });
255
+
256
+ test('test 37 - universal selector with class', () => {
257
+ const res = calculateSpecificity('*[role="button"].btn');
258
+ expect(res.A).toBe(0);
259
+ expect(res.B).toBe(2); // [role] + .btn
260
+ expect(res.C).toBe(0); // universal selector doesn't count
261
+ });
262
+
263
+ test('test 38 - nested :has()', () => {
264
+ const res = calculateSpecificity('div:has(> span.highlight, a#link)');
265
+ expect(res.A).toBe(1); // #link (most specific argument: a#link)
266
+ expect(res.B).toBe(0);
267
+ expect(res.C).toBe(2); // div + a
268
+ });
269
+
270
+ test('test 39 - :where() does not increase specificity', () => {
271
+ const res = calculateSpecificity(':where(.a, #b)');
272
+ expect(res.A).toBe(0);
273
+ expect(res.B).toBe(0);
274
+ expect(res.C).toBe(0);
275
+ });
276
+
277
+ test('test 40 - type selector and namespace', () => {
278
+ const res = calculateSpecificity('svg|circle.special');
279
+ expect(res.A).toBe(0);
280
+ expect(res.B).toBe(1); // .special
281
+ expect(res.C).toBe(1); // circle type
282
+ });
283
+
284
+ test('test 41 - multiple combinators and pseudo-elements', () => {
285
+ const res = calculateSpecificity('header nav > ul li::after');
286
+ expect(res.A).toBe(0);
287
+ expect(res.B).toBe(0);
288
+ expect(res.C).toBe(5); // header + nav + ul + li + ::after
289
+ });
290
+
291
+ test('test 42 - :nth-last-child with of selector list', () => {
292
+ const res = calculateSpecificity(':nth-last-child(3 of .x, #y, a)');
293
+ expect(res.A).toBe(1); // max from #y
294
+ expect(res.B).toBe(1); // nth-last-child itself
295
+ expect(res.C).toBe(0);
296
+ });
297
+
298
+ test('test 43 - escaping in identifiers', () => {
299
+ const res = calculateSpecificity('.class\\#escaped #id\\:special');
300
+ expect(res.A).toBe(1); // #id\:special
301
+ expect(res.B).toBe(1); // .class\#escaped
302
+ expect(res.C).toBe(0);
303
+ });
304
+
305
+ test('test 44 - nested functional pseudo-classes', () => {
306
+ const res = calculateSpecificity(':is(:not(.a), :has(#b))');
307
+ expect(res.A).toBe(1); // #b (most specific :is arg: :has(#b) = (1,0,0))
308
+ expect(res.B).toBe(0);
309
+ expect(res.C).toBe(0);
310
+ });
311
+
312
+ test('test 45 - complex all together', () => {
313
+ const res = calculateSpecificity('body > header.navbar :is(ul li:first-child, a#link.active):hover');
314
+ expect(res.A).toBe(1); // #link (most specific :is arg: a#link.active)
315
+ expect(res.B).toBe(3); // .navbar + .active + :hover
316
+ expect(res.C).toBe(3); // body + header + a
317
+ });
318
+
319
+ test('test 46 - deeply nested :is() and :not()', () => {
320
+ const res = calculateSpecificity(':is(:not(.a, #b), .c)');
321
+ expect(res.A).toBe(1); // #b (most specific :is arg: :not(.a, #b) = (1,0,0))
322
+ expect(res.B).toBe(0);
323
+ expect(res.C).toBe(0);
324
+ });
325
+
326
+ test('test 47 - multiple combinators with pseudo-classes', () => {
327
+ const res = calculateSpecificity('div > ul li:first-child.active + a:hover');
328
+ expect(res.A).toBe(0);
329
+ expect(res.B).toBe(3); // .active + :first-child + :hover
330
+ expect(res.C).toBe(4); // div + ul + li + a
331
+ });
332
+
333
+ test('test 48 - :slotted pseudo-class', () => {
334
+ const res = calculateSpecificity(':slotted(.item#id)');
335
+ expect(res.A).toBe(1); // #id
336
+ expect(res.B).toBe(1); // .item
337
+ expect(res.C).toBe(0);
338
+ });
339
+
340
+ test('test 49 - :host() pseudo-class', () => {
341
+ const res = calculateSpecificity(':host(.container)');
342
+ expect(res.A).toBe(0);
343
+ expect(res.B).toBe(2); // :host pseudo-class + .container
344
+ expect(res.C).toBe(0);
345
+ });
346
+
347
+ test('test 50 - :host-context() pseudo-class', () => {
348
+ const res = calculateSpecificity(':host-context(#parent) .child');
349
+ expect(res.A).toBe(1); // #parent
350
+ expect(res.B).toBe(2); // :host-context pseudo-class + .child
351
+ expect(res.C).toBe(0);
352
+ });
353
+
354
+ test('test 51 - multiple pseudo-elements in chain', () => {
355
+ const res = calculateSpecificity('div::first-letter::after');
356
+ expect(res.A).toBe(0);
357
+ expect(res.B).toBe(0);
358
+ expect(res.C).toBe(3); // div + ::first-letter + ::after
359
+ });
360
+
361
+ test('test 52 - universal + class + pseudo', () => {
362
+ const res = calculateSpecificity('*:hover.active');
363
+ expect(res.A).toBe(0);
364
+ expect(res.B).toBe(2); // .active + :hover
365
+ expect(res.C).toBe(0); // * doesn't count
366
+ });
367
+
368
+ test('test 53 - type + class + attribute', () => {
369
+ const res = calculateSpecificity('button.btn[type="submit"]');
370
+ expect(res.A).toBe(0);
371
+ expect(res.B).toBe(2); // .btn + [type]
372
+ expect(res.C).toBe(1); // button
373
+ });
374
+
375
+ test('test 54 - deeply nested :has()', () => {
376
+ const res = calculateSpecificity('div:has(ul li:first-child, a#link)');
377
+ expect(res.A).toBe(1); // #link (most specific argument: a#link)
378
+ expect(res.B).toBe(0);
379
+ expect(res.C).toBe(2); // div + a
380
+ });
381
+
382
+ test('test 55 - multiple :where()', () => {
383
+ const res = calculateSpecificity(':where(.a, #b):where(.c, div)');
384
+ expect(res.A).toBe(0);
385
+ expect(res.B).toBe(0);
386
+ expect(res.C).toBe(0);
387
+ });
388
+
389
+ test('test 56 - type selector + namespace', () => {
390
+ const res = calculateSpecificity('html|body main|article.section');
391
+ expect(res.A).toBe(0);
392
+ expect(res.B).toBe(1); // .section
393
+ expect(res.C).toBe(2); // body + article (namespaced type selectors)
394
+ });
395
+
396
+ test('test 57 - multiple descendant combinators', () => {
397
+ const res = calculateSpecificity('header nav ul li a.link:hover');
398
+ expect(res.A).toBe(0);
399
+ expect(res.B).toBe(2); // .link + :hover
400
+ expect(res.C).toBe(5); // header + nav + ul + li + a
401
+ });
402
+
403
+ test('test 58 - :not() with type + class', () => {
404
+ const res = calculateSpecificity(':not(div.item)');
405
+ expect(res.A).toBe(0);
406
+ expect(res.B).toBe(1); // .item
407
+ expect(res.C).toBe(1); // div
408
+ });
409
+
410
+ test('test 59 - :is() inside :has()', () => {
411
+ const res = calculateSpecificity('section:has(:is(.a, #b))');
412
+ expect(res.A).toBe(1); // #b (most specific :is arg: #b = (1,0,0))
413
+ expect(res.B).toBe(0);
414
+ expect(res.C).toBe(1); // section
415
+ });
416
+
417
+ test('test 60 - multiple functional pseudo-classes', () => {
418
+ const res = calculateSpecificity(':not(:is(.a, #b)):has(.c)');
419
+ expect(res.A).toBe(1); // #b (most specific :is arg: #b = (1,0,0))
420
+ expect(res.B).toBe(1); // .c from :has()
421
+ expect(res.C).toBe(0);
422
+ });
423
+
424
+ // --- Legacy single-colon pseudo-elements ---
425
+
426
+ test('test 61 - legacy :before pseudo-element', () => {
427
+ const res = calculateSpecificity('p:before');
428
+ expect(res.A).toBe(0);
429
+ expect(res.B).toBe(0);
430
+ expect(res.C).toBe(2); // p + :before (pseudo-element)
431
+ });
432
+
433
+ test('test 62 - legacy :after pseudo-element', () => {
434
+ const res = calculateSpecificity('div.item:after');
435
+ expect(res.A).toBe(0);
436
+ expect(res.B).toBe(1); // .item
437
+ expect(res.C).toBe(2); // div + :after
438
+ });
439
+
440
+ test('test 63 - legacy :first-line pseudo-element', () => {
441
+ const res = calculateSpecificity('p:first-line');
442
+ expect(res.A).toBe(0);
443
+ expect(res.B).toBe(0);
444
+ expect(res.C).toBe(2); // p + :first-line
445
+ });
446
+
447
+ test('test 64 - legacy :first-letter pseudo-element', () => {
448
+ const res = calculateSpecificity('p:first-letter');
449
+ expect(res.A).toBe(0);
450
+ expect(res.B).toBe(0);
451
+ expect(res.C).toBe(2); // p + :first-letter
452
+ });
453
+
454
+ // --- :matches() and vendor-prefixed :any() ---
455
+
456
+ test('test 65 - :matches() behaves like :is()', () => {
457
+ const res = calculateSpecificity(':matches(.a, #b)');
458
+ expect(res.A).toBe(1); // most specific arg: #b
459
+ expect(res.B).toBe(0);
460
+ expect(res.C).toBe(0);
461
+ });
462
+
463
+ test('test 66 - :-webkit-any() behaves like :is()', () => {
464
+ const res = calculateSpecificity(':-webkit-any(.a, #b)');
465
+ expect(res.A).toBe(1);
466
+ expect(res.B).toBe(0);
467
+ expect(res.C).toBe(0);
468
+ });
469
+
470
+ test('test 67 - :-moz-any() behaves like :is()', () => {
471
+ const res = calculateSpecificity(':-moz-any(.a, #b)');
472
+ expect(res.A).toBe(1);
473
+ expect(res.B).toBe(0);
474
+ expect(res.C).toBe(0);
475
+ });
476
+
477
+ // --- Column combinator || ---
478
+
479
+ test('test 68 - column combinator ||', () => {
480
+ const res = calculateSpecificity('col.selected || td');
481
+ expect(res.A).toBe(0);
482
+ expect(res.B).toBe(1); // .selected
483
+ expect(res.C).toBe(2); // col + td
484
+ });
485
+
486
+ // --- Nesting selector & ---
487
+
488
+ test('test 69 - bare nesting selector & has zero specificity', () => {
489
+ const res = calculateSpecificity('&');
490
+ expect(res.A).toBe(0);
491
+ expect(res.B).toBe(0);
492
+ expect(res.C).toBe(0);
493
+ });
494
+
495
+ test('test 70 - & with class', () => {
496
+ const res = calculateSpecificity('&.active');
497
+ expect(res.A).toBe(0);
498
+ expect(res.B).toBe(1); // .active
499
+ expect(res.C).toBe(0); // & has zero specificity on its own
500
+ });
501
+
502
+ test('test 71 - & as descendant', () => {
503
+ const res = calculateSpecificity('& .child');
504
+ expect(res.A).toBe(0);
505
+ expect(res.B).toBe(1); // .child
506
+ expect(res.C).toBe(0); // & has zero specificity on its own
507
+ });
508
+
509
+ test('test 72 - & with pseudo-class', () => {
510
+ const res = calculateSpecificity('&:hover');
511
+ expect(res.A).toBe(0);
512
+ expect(res.B).toBe(1); // :hover
513
+ expect(res.C).toBe(0); // & has zero specificity on its own
514
+ });
515
+
516
+ test('test 73 - & with pseudo-element', () => {
517
+ const res = calculateSpecificity('&::before');
518
+ expect(res.A).toBe(0);
519
+ expect(res.B).toBe(0);
520
+ expect(res.C).toBe(1); // ::before
521
+ });
522
+
523
+ // --- Performance test ---
524
+
525
+ test('performance - 100k iterations across selector categories', () => {
526
+ const selectors = {
527
+ simple: [
528
+ 'div',
529
+ '.btn',
530
+ '#main',
531
+ 'div.container',
532
+ 'ul li a.link',
533
+ 'body > header nav ul li',
534
+ '#app .sidebar .nav-item',
535
+ 'main > section article p span',
536
+ ],
537
+ withAttributes: [
538
+ '[data-id]',
539
+ 'input[type="text"]',
540
+ 'a[href^="https"][target="_blank"].external',
541
+ 'div[class~="active"][role="button"]',
542
+ ],
543
+ withPseudoClasses: [
544
+ 'a:hover',
545
+ 'div:first-child',
546
+ 'li:nth-child(2n+1)',
547
+ 'input:focus:not(:disabled)',
548
+ 'tr:nth-child(odd):hover',
549
+ ],
550
+ withPseudoElements: [
551
+ 'p::before',
552
+ 'div::after',
553
+ 'p:before',
554
+ 'h1::first-line',
555
+ 'blockquote::first-letter',
556
+ ],
557
+ withIsNotHas: [
558
+ ':is(.a, .b, .c)',
559
+ ':not(#main)',
560
+ ':has(> .child)',
561
+ ':is(.nav, #sidebar):not(.hidden)',
562
+ 'div:has(> span.highlight, a#link)',
563
+ ':is(:not(.a, #b), .c)',
564
+ ],
565
+ withWhere: [
566
+ ':where(.a, #b)',
567
+ ':where(div, span):is(.active)',
568
+ ],
569
+ withHostSlotted: [
570
+ ':host(.container)',
571
+ ':host-context(#parent) .child',
572
+ ':slotted(.item#id)',
573
+ ],
574
+ complex: [
575
+ 'body > header.navbar :is(ul li:first-child, a#link.active):hover',
576
+ ':not(:is(.a, #b)):has(.c)',
577
+ 'section:has(:is(.a, #b))',
578
+ 'div > ul li:first-child.active + a:hover',
579
+ 'col.selected || td',
580
+ ],
581
+ };
582
+
583
+ const allSelectors = Object.values(selectors).flat();
584
+ const iterations = 100_000;
585
+
586
+ // Warmup
587
+ for (let i = 0; i < 1000; i++) {
588
+ for (const sel of allSelectors) calculateSpecificity(sel);
589
+ }
590
+
591
+ const categoryResults: Record<string, { ops: number; nsPerOp: number }> = {};
592
+
593
+ // Benchmark per category
594
+ for (const [category, sels] of Object.entries(selectors)) {
595
+ const start = performance.now();
596
+ for (let i = 0; i < iterations; i++) {
597
+ for (const sel of sels) calculateSpecificity(sel);
598
+ }
599
+ const elapsed = performance.now() - start;
600
+ const totalOps = iterations * sels.length;
601
+ categoryResults[category] = {
602
+ ops: totalOps,
603
+ nsPerOp: (elapsed * 1_000_000) / totalOps,
604
+ };
605
+ }
606
+
607
+ // Overall benchmark
608
+ const overallStart = performance.now();
609
+ for (let i = 0; i < iterations; i++) {
610
+ for (const sel of allSelectors) calculateSpecificity(sel);
611
+ }
612
+ const overallElapsed = performance.now() - overallStart;
613
+ const totalOps = iterations * allSelectors.length;
614
+
615
+ // Print results
616
+ console.log('\n── Specificity Calculator Performance ──');
617
+ console.log(`Total: ${totalOps.toLocaleString()} ops in ${overallElapsed.toFixed(1)}ms (${((overallElapsed * 1_000_000) / totalOps).toFixed(0)}ns/op)\n`);
618
+ for (const [category, result] of Object.entries(categoryResults)) {
619
+ console.log(` ${category.padEnd(22)} ${result.nsPerOp.toFixed(0).padStart(5)}ns/op (${result.ops.toLocaleString()} ops)`);
620
+ }
621
+ console.log('');
622
+
623
+ // Sanity check: should complete in reasonable time (< 5 seconds total)
624
+ expect(overallElapsed).toBeLessThan(5000);
625
+ });