@luna_ui/luna 0.3.4 → 0.3.5
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/dist/cli.mjs +1264 -27
- package/dist/css/index.d.ts +194 -0
- package/dist/css/index.js +721 -0
- package/dist/css/runtime.d.ts +92 -0
- package/dist/css/runtime.js +179 -0
- package/dist/index.js +1 -1
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/{src-BDdxGwvq.js → src-CHiGeWfy.js} +1 -1
- package/dist/vite-plugin.d.ts +122 -0
- package/dist/vite-plugin.js +1518 -0
- package/package.json +16 -2
- package/src/css/extract.ts +798 -0
- package/src/css/index.ts +10 -0
- package/src/css/inject.ts +205 -0
- package/src/css/inline.ts +182 -0
- package/src/css/minify.ts +70 -0
- package/src/css/optimizer.ts +6 -0
- package/src/css/runtime.ts +344 -0
- package/src/css-optimizer/README.md +353 -0
- package/src/css-optimizer/cooccurrence.ts +100 -0
- package/src/css-optimizer/core.ts +263 -0
- package/src/css-optimizer/extractors.ts +243 -0
- package/src/css-optimizer/hash.ts +54 -0
- package/src/css-optimizer/index.ts +129 -0
- package/src/css-optimizer/merge.ts +109 -0
- package/src/css-optimizer/moonbit-analyzer.ts +210 -0
- package/src/css-optimizer/parser.ts +120 -0
- package/src/css-optimizer/pattern.ts +171 -0
- package/src/css-optimizer/transformers.ts +301 -0
- package/src/css-optimizer/types.ts +128 -0
- package/src/event-utils.ts +227 -0
- package/src/index.ts +890 -0
- package/src/jsx-dev-runtime.ts +2 -0
- package/src/jsx-runtime.ts +398 -0
- package/src/vite-plugin.ts +718 -0
- package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-empty-to-non-empty-transition-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-renders-correctly-without-show--initial-items--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-with-context-renders-correctly-without-show-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-nested-components-with-context--forEach--and-show-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-inherit-context-from-Owner--fixed--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-work-together--context-uses-default--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-works-with-For-loop-items--component-body-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
- package/tests/context.test.ts +118 -0
- package/tests/css-optimizer-extractors.test.ts +264 -0
- package/tests/css-optimizer-integration.test.ts +566 -0
- package/tests/css-optimizer-transformers.test.ts +301 -0
- package/tests/css-optimizer.test.ts +646 -0
- package/tests/css-runtime.bench.ts +442 -0
- package/tests/css-runtime.test.ts +342 -0
- package/tests/dom.test.ts +872 -0
- package/tests/integration.test.ts +405 -0
- package/tests/issue-5-for-infinite-loop.test.ts +516 -0
- package/tests/jsx-runtime.test.tsx +393 -0
- package/tests/lifecycle.test.ts +833 -0
- package/tests/move-before.bench.ts +304 -0
- package/tests/preact-signals-comparison.test.ts +1608 -0
- package/tests/resource.test.ts +160 -0
- package/tests/router.test.ts +117 -0
- package/tests/show-initial-mount-leak.test.tsx +182 -0
- package/tests/solidjs-api.test.ts +659 -0
- package/tests/static-perf.bench.ts +64 -0
- package/tests/store.test.ts +263 -0
- package/tests/tsx-syntax.test.tsx +404 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive tests for CSS Co-occurrence Optimizer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
// Hash utilities
|
|
8
|
+
djb2Hash,
|
|
9
|
+
toBase36,
|
|
10
|
+
hashClassName,
|
|
11
|
+
hashMergedClassName,
|
|
12
|
+
// Parsing
|
|
13
|
+
extractClassUsages,
|
|
14
|
+
parseCssRules,
|
|
15
|
+
buildClassToDeclarationMap,
|
|
16
|
+
extractUniqueClasses,
|
|
17
|
+
// Co-occurrence
|
|
18
|
+
buildCooccurrenceMatrix,
|
|
19
|
+
matrixToCooccurrences,
|
|
20
|
+
getTopCooccurrences,
|
|
21
|
+
buildAdjacencyList,
|
|
22
|
+
// Pattern mining
|
|
23
|
+
findFrequentPatterns,
|
|
24
|
+
removeSubsumedPatterns,
|
|
25
|
+
groupByClassSet,
|
|
26
|
+
// Main API
|
|
27
|
+
optimizeCss,
|
|
28
|
+
optimizeHtml,
|
|
29
|
+
} from "../src/css-optimizer/index.js";
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Hash Tests
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
describe("Hash utilities", () => {
|
|
36
|
+
test("djb2Hash produces consistent hashes", () => {
|
|
37
|
+
expect(djb2Hash("display:flex")).toBe(djb2Hash("display:flex"));
|
|
38
|
+
expect(djb2Hash("display:flex")).not.toBe(djb2Hash("display:block"));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("djb2Hash handles empty string", () => {
|
|
42
|
+
expect(djb2Hash("")).toBe(5381);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("toBase36 converts correctly", () => {
|
|
46
|
+
expect(toBase36(0)).toBe("0");
|
|
47
|
+
expect(toBase36(35)).toBe("z");
|
|
48
|
+
expect(toBase36(36)).toBe("10");
|
|
49
|
+
expect(toBase36(100)).toBe("2s");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("hashClassName generates prefixed class names", () => {
|
|
53
|
+
const cls = hashClassName("display:flex");
|
|
54
|
+
expect(cls).toMatch(/^_[a-z0-9]+$/);
|
|
55
|
+
expect(cls.length).toBeGreaterThan(1);
|
|
56
|
+
expect(cls.length).toBeLessThanOrEqual(8);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("hashClassName with custom prefix", () => {
|
|
60
|
+
const cls = hashClassName("display:flex", "css-");
|
|
61
|
+
expect(cls).toMatch(/^css-[a-z0-9]+$/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("hashMergedClassName generates merged class names", () => {
|
|
65
|
+
const cls = hashMergedClassName(["display:flex", "color:red"]);
|
|
66
|
+
expect(cls).toMatch(/^_m[a-z0-9]+$/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("hashMergedClassName is order-independent", () => {
|
|
70
|
+
const cls1 = hashMergedClassName(["display:flex", "color:red"]);
|
|
71
|
+
const cls2 = hashMergedClassName(["color:red", "display:flex"]);
|
|
72
|
+
expect(cls1).toBe(cls2);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Parser Tests
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
80
|
+
describe("Parser utilities", () => {
|
|
81
|
+
describe("extractClassUsages", () => {
|
|
82
|
+
test("extracts class combinations from HTML", () => {
|
|
83
|
+
const html = `
|
|
84
|
+
<div class="_a _b _c">Content</div>
|
|
85
|
+
<span class="_x _y">Other</span>
|
|
86
|
+
`;
|
|
87
|
+
const usages = extractClassUsages(html);
|
|
88
|
+
|
|
89
|
+
expect(usages).toHaveLength(2);
|
|
90
|
+
expect(usages[0].classes).toEqual(["_a", "_b", "_c"]);
|
|
91
|
+
expect(usages[1].classes).toEqual(["_x", "_y"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("ignores single class elements", () => {
|
|
95
|
+
const html = `<div class="_single">Content</div>`;
|
|
96
|
+
const usages = extractClassUsages(html);
|
|
97
|
+
expect(usages).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("ignores non-prefixed classes", () => {
|
|
101
|
+
const html = `<div class="normal-class _luna another">Content</div>`;
|
|
102
|
+
const usages = extractClassUsages(html, "html", "_");
|
|
103
|
+
expect(usages).toHaveLength(0); // Only 1 _-prefixed class
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("sorts classes alphabetically", () => {
|
|
107
|
+
const html = `<div class="_z _a _m">Content</div>`;
|
|
108
|
+
const usages = extractClassUsages(html);
|
|
109
|
+
expect(usages[0].classes).toEqual(["_a", "_m", "_z"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("uses custom prefix filter", () => {
|
|
113
|
+
const html = `<div class="tw-a tw-b regular">Content</div>`;
|
|
114
|
+
const usages = extractClassUsages(html, "html", "tw-");
|
|
115
|
+
expect(usages).toHaveLength(1);
|
|
116
|
+
expect(usages[0].classes).toEqual(["tw-a", "tw-b"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("includes source location", () => {
|
|
120
|
+
const html = `<div class="_a _b">Content</div>`;
|
|
121
|
+
const usages = extractClassUsages(html, "test.html");
|
|
122
|
+
expect(usages[0].source).toContain("test.html");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("parseCssRules", () => {
|
|
127
|
+
test("parses simple CSS rules", () => {
|
|
128
|
+
const css = "._a{display:flex}._b{color:red}";
|
|
129
|
+
const rules = parseCssRules(css);
|
|
130
|
+
|
|
131
|
+
expect(rules).toHaveLength(2);
|
|
132
|
+
expect(rules[0]).toEqual({ selector: "_a", declarations: "display:flex" });
|
|
133
|
+
expect(rules[1]).toEqual({ selector: "_b", declarations: "color:red" });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("handles multi-declaration rules", () => {
|
|
137
|
+
const css = "._a{display:flex;align-items:center;gap:10px}";
|
|
138
|
+
const rules = parseCssRules(css);
|
|
139
|
+
|
|
140
|
+
expect(rules).toHaveLength(1);
|
|
141
|
+
expect(rules[0].declarations).toBe("display:flex;align-items:center;gap:10px");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("handles whitespace in CSS", () => {
|
|
145
|
+
const css = "._a { display: flex; }";
|
|
146
|
+
const rules = parseCssRules(css);
|
|
147
|
+
|
|
148
|
+
expect(rules).toHaveLength(1);
|
|
149
|
+
expect(rules[0].declarations.trim()).toContain("display");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("buildClassToDeclarationMap", () => {
|
|
154
|
+
test("builds correct mapping", () => {
|
|
155
|
+
const css = "._a{display:flex}._b{color:red}";
|
|
156
|
+
const map = buildClassToDeclarationMap(css);
|
|
157
|
+
|
|
158
|
+
expect(map.get("_a")).toBe("display:flex");
|
|
159
|
+
expect(map.get("_b")).toBe("color:red");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("filters by prefix", () => {
|
|
163
|
+
const css = "._a{display:flex}.normal{color:red}";
|
|
164
|
+
const map = buildClassToDeclarationMap(css, "_");
|
|
165
|
+
|
|
166
|
+
expect(map.has("_a")).toBe(true);
|
|
167
|
+
expect(map.has("normal")).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("extractUniqueClasses", () => {
|
|
172
|
+
test("extracts all unique classes", () => {
|
|
173
|
+
const html = `
|
|
174
|
+
<div class="_a _b _c">Content</div>
|
|
175
|
+
<span class="_b _d">Other</span>
|
|
176
|
+
`;
|
|
177
|
+
const classes = extractUniqueClasses(html);
|
|
178
|
+
|
|
179
|
+
expect(classes.size).toBe(4);
|
|
180
|
+
expect(classes.has("_a")).toBe(true);
|
|
181
|
+
expect(classes.has("_b")).toBe(true);
|
|
182
|
+
expect(classes.has("_c")).toBe(true);
|
|
183
|
+
expect(classes.has("_d")).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// =============================================================================
|
|
189
|
+
// Co-occurrence Tests
|
|
190
|
+
// =============================================================================
|
|
191
|
+
|
|
192
|
+
describe("Co-occurrence analysis", () => {
|
|
193
|
+
describe("buildCooccurrenceMatrix", () => {
|
|
194
|
+
test("counts pair co-occurrences", () => {
|
|
195
|
+
const usages = [
|
|
196
|
+
{ classes: ["_a", "_b", "_c"], source: "test1" },
|
|
197
|
+
{ classes: ["_a", "_b"], source: "test2" },
|
|
198
|
+
{ classes: ["_a", "_c"], source: "test3" },
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const matrix = buildCooccurrenceMatrix(usages);
|
|
202
|
+
|
|
203
|
+
// _a and _b appear together 2 times
|
|
204
|
+
expect(matrix.get("_a")?.get("_b")).toBe(2);
|
|
205
|
+
// _a and _c appear together 2 times
|
|
206
|
+
expect(matrix.get("_a")?.get("_c")).toBe(2);
|
|
207
|
+
// _b and _c appear together 1 time
|
|
208
|
+
expect(matrix.get("_b")?.get("_c")).toBe(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("maintains alphabetical order", () => {
|
|
212
|
+
const usages = [{ classes: ["_z", "_a"], source: "test" }];
|
|
213
|
+
const matrix = buildCooccurrenceMatrix(usages);
|
|
214
|
+
|
|
215
|
+
// Should be stored as _a -> _z, not _z -> _a
|
|
216
|
+
expect(matrix.has("_a")).toBe(true);
|
|
217
|
+
expect(matrix.get("_a")?.has("_z")).toBe(true);
|
|
218
|
+
expect(matrix.has("_z")).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("handles empty input", () => {
|
|
222
|
+
const matrix = buildCooccurrenceMatrix([]);
|
|
223
|
+
expect(matrix.size).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("matrixToCooccurrences", () => {
|
|
228
|
+
test("converts matrix to sorted array", () => {
|
|
229
|
+
const usages = [
|
|
230
|
+
{ classes: ["_a", "_b"], source: "1" },
|
|
231
|
+
{ classes: ["_a", "_b"], source: "2" },
|
|
232
|
+
{ classes: ["_x", "_y"], source: "3" },
|
|
233
|
+
];
|
|
234
|
+
const matrix = buildCooccurrenceMatrix(usages);
|
|
235
|
+
const cooccurrences = matrixToCooccurrences(matrix);
|
|
236
|
+
|
|
237
|
+
expect(cooccurrences[0].frequency).toBeGreaterThanOrEqual(cooccurrences[1]?.frequency || 0);
|
|
238
|
+
expect(cooccurrences.find((c) => c.classA === "_a" && c.classB === "_b")?.frequency).toBe(2);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("getTopCooccurrences", () => {
|
|
243
|
+
test("returns top N pairs", () => {
|
|
244
|
+
const cooccurrences = [
|
|
245
|
+
{ classA: "_a", classB: "_b", frequency: 10 },
|
|
246
|
+
{ classA: "_c", classB: "_d", frequency: 5 },
|
|
247
|
+
{ classA: "_e", classB: "_f", frequency: 3 },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
const top2 = getTopCooccurrences(cooccurrences, 2);
|
|
251
|
+
expect(top2).toHaveLength(2);
|
|
252
|
+
expect(top2[0].frequency).toBe(10);
|
|
253
|
+
expect(top2[1].frequency).toBe(5);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("buildAdjacencyList", () => {
|
|
258
|
+
test("builds bidirectional adjacency list", () => {
|
|
259
|
+
const cooccurrences = [{ classA: "_a", classB: "_b", frequency: 5 }];
|
|
260
|
+
const adj = buildAdjacencyList(cooccurrences, 1);
|
|
261
|
+
|
|
262
|
+
expect(adj.get("_a")?.find((e) => e.target === "_b")).toBeDefined();
|
|
263
|
+
expect(adj.get("_b")?.find((e) => e.target === "_a")).toBeDefined();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("respects minFrequency filter", () => {
|
|
267
|
+
const cooccurrences = [
|
|
268
|
+
{ classA: "_a", classB: "_b", frequency: 5 },
|
|
269
|
+
{ classA: "_c", classB: "_d", frequency: 1 },
|
|
270
|
+
];
|
|
271
|
+
const adj = buildAdjacencyList(cooccurrences, 3);
|
|
272
|
+
|
|
273
|
+
expect(adj.has("_a")).toBe(true);
|
|
274
|
+
expect(adj.has("_c")).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// =============================================================================
|
|
280
|
+
// Pattern Mining Tests
|
|
281
|
+
// =============================================================================
|
|
282
|
+
|
|
283
|
+
describe("Pattern mining", () => {
|
|
284
|
+
describe("findFrequentPatterns", () => {
|
|
285
|
+
test("finds pair patterns", () => {
|
|
286
|
+
const usages = [
|
|
287
|
+
{ classes: ["_a", "_b"], source: "1" },
|
|
288
|
+
{ classes: ["_a", "_b"], source: "2" },
|
|
289
|
+
{ classes: ["_c", "_d"], source: "3" },
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
const patterns = findFrequentPatterns(usages, 2, 2);
|
|
293
|
+
|
|
294
|
+
expect(patterns.length).toBeGreaterThanOrEqual(1);
|
|
295
|
+
const abPattern = patterns.find(
|
|
296
|
+
(p) => p.originalClasses.includes("_a") && p.originalClasses.includes("_b")
|
|
297
|
+
);
|
|
298
|
+
expect(abPattern).toBeDefined();
|
|
299
|
+
expect(abPattern?.frequency).toBe(2);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("finds triple patterns", () => {
|
|
303
|
+
const usages = [
|
|
304
|
+
{ classes: ["_a", "_b", "_c"], source: "1" },
|
|
305
|
+
{ classes: ["_a", "_b", "_c"], source: "2" },
|
|
306
|
+
{ classes: ["_a", "_b", "_c"], source: "3" },
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const patterns = findFrequentPatterns(usages, 3, 3);
|
|
310
|
+
|
|
311
|
+
const triplePattern = patterns.find((p) => p.originalClasses.length === 3);
|
|
312
|
+
expect(triplePattern).toBeDefined();
|
|
313
|
+
expect(triplePattern?.frequency).toBe(3);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("respects minFrequency", () => {
|
|
317
|
+
const usages = [
|
|
318
|
+
{ classes: ["_a", "_b"], source: "1" },
|
|
319
|
+
{ classes: ["_c", "_d"], source: "2" },
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
const patterns = findFrequentPatterns(usages, 2, 2);
|
|
323
|
+
expect(patterns.length).toBe(0); // Each pair appears only once
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("respects maxPatternSize", () => {
|
|
327
|
+
const usages = [
|
|
328
|
+
{ classes: ["_a", "_b", "_c", "_d"], source: "1" },
|
|
329
|
+
{ classes: ["_a", "_b", "_c", "_d"], source: "2" },
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const patterns2 = findFrequentPatterns(usages, 2, 2);
|
|
333
|
+
const patterns4 = findFrequentPatterns(usages, 2, 4);
|
|
334
|
+
|
|
335
|
+
expect(patterns2.every((p) => p.originalClasses.length <= 2)).toBe(true);
|
|
336
|
+
expect(patterns4.some((p) => p.originalClasses.length > 2)).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("calculates bytesSaved estimate", () => {
|
|
340
|
+
const usages = [
|
|
341
|
+
{ classes: ["_a", "_b", "_c"], source: "1" },
|
|
342
|
+
{ classes: ["_a", "_b", "_c"], source: "2" },
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
const patterns = findFrequentPatterns(usages, 2, 3);
|
|
346
|
+
const pattern = patterns.find((p) => p.originalClasses.length === 3);
|
|
347
|
+
|
|
348
|
+
expect(pattern?.bytesSaved).toBeGreaterThan(0);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("removeSubsumedPatterns", () => {
|
|
353
|
+
test("removes subset patterns", () => {
|
|
354
|
+
const patterns = [
|
|
355
|
+
{ originalClasses: ["_a", "_b"], frequency: 5, mergedClass: "", declarations: [], bytesSaved: 100 },
|
|
356
|
+
{ originalClasses: ["_a", "_b", "_c"], frequency: 5, mergedClass: "", declarations: [], bytesSaved: 200 },
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
const result = removeSubsumedPatterns(patterns);
|
|
360
|
+
|
|
361
|
+
// Should keep only the larger pattern
|
|
362
|
+
expect(result.length).toBe(1);
|
|
363
|
+
expect(result[0].originalClasses).toEqual(["_a", "_b", "_c"]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("keeps patterns with significantly different frequencies", () => {
|
|
367
|
+
const patterns = [
|
|
368
|
+
{ originalClasses: ["_a", "_b"], frequency: 10, mergedClass: "", declarations: [], bytesSaved: 100 },
|
|
369
|
+
{ originalClasses: ["_a", "_b", "_c"], frequency: 2, mergedClass: "", declarations: [], bytesSaved: 50 },
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const result = removeSubsumedPatterns(patterns);
|
|
373
|
+
|
|
374
|
+
// Should keep both because frequency difference is significant
|
|
375
|
+
expect(result.length).toBe(2);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("groupByClassSet", () => {
|
|
380
|
+
test("groups usages by exact class set", () => {
|
|
381
|
+
const usages = [
|
|
382
|
+
{ classes: ["_a", "_b"], source: "1" },
|
|
383
|
+
{ classes: ["_a", "_b"], source: "2" },
|
|
384
|
+
{ classes: ["_c", "_d"], source: "3" },
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
const groups = groupByClassSet(usages);
|
|
388
|
+
|
|
389
|
+
expect(groups.size).toBe(2);
|
|
390
|
+
expect(groups.get("_a|_b")?.length).toBe(2);
|
|
391
|
+
expect(groups.get("_c|_d")?.length).toBe(1);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// =============================================================================
|
|
397
|
+
// Main API Tests
|
|
398
|
+
// =============================================================================
|
|
399
|
+
|
|
400
|
+
describe("Main API", () => {
|
|
401
|
+
describe("optimizeCss", () => {
|
|
402
|
+
test("merges frequently co-occurring classes", () => {
|
|
403
|
+
const css = "._a{display:flex}._b{align-items:center}._c{color:red}";
|
|
404
|
+
const html = `
|
|
405
|
+
<div class="_a _b">Content</div>
|
|
406
|
+
<div class="_a _b">More</div>
|
|
407
|
+
<span class="_c">Other</span>
|
|
408
|
+
`;
|
|
409
|
+
const mapping = {
|
|
410
|
+
"display:flex": "_a",
|
|
411
|
+
"align-items:center": "_b",
|
|
412
|
+
"color:red": "_c",
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
416
|
+
|
|
417
|
+
expect(result.patterns.length).toBeGreaterThanOrEqual(1);
|
|
418
|
+
expect(result.mergeMap.size).toBeGreaterThanOrEqual(1);
|
|
419
|
+
expect(result.stats.mergedPatterns).toBeGreaterThanOrEqual(1);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("generates valid merged CSS", () => {
|
|
423
|
+
const css = "._a{display:flex}._b{align-items:center}";
|
|
424
|
+
const html = `
|
|
425
|
+
<div class="_a _b">1</div>
|
|
426
|
+
<div class="_a _b">2</div>
|
|
427
|
+
`;
|
|
428
|
+
const mapping = {
|
|
429
|
+
"display:flex": "_a",
|
|
430
|
+
"align-items:center": "_b",
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
434
|
+
|
|
435
|
+
// Check that merged class rule is generated
|
|
436
|
+
if (result.patterns.length > 0) {
|
|
437
|
+
const mergedClass = result.patterns[0].mergedClass;
|
|
438
|
+
expect(result.css).toContain(`.${mergedClass}{`);
|
|
439
|
+
expect(result.css).toContain("display:flex");
|
|
440
|
+
expect(result.css).toContain("align-items:center");
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("removes merged classes from original CSS", () => {
|
|
445
|
+
const css = "._a{display:flex}._b{align-items:center}._c{color:red}";
|
|
446
|
+
const html = `
|
|
447
|
+
<div class="_a _b">1</div>
|
|
448
|
+
<div class="_a _b">2</div>
|
|
449
|
+
`;
|
|
450
|
+
const mapping = {
|
|
451
|
+
"display:flex": "_a",
|
|
452
|
+
"align-items:center": "_b",
|
|
453
|
+
"color:red": "_c",
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
457
|
+
|
|
458
|
+
if (result.patterns.length > 0) {
|
|
459
|
+
// Merged classes should be removed
|
|
460
|
+
expect(result.css).not.toMatch(/\._a\{display:flex\}/);
|
|
461
|
+
expect(result.css).not.toMatch(/\._b\{align-items:center\}/);
|
|
462
|
+
// Unmerged class should remain
|
|
463
|
+
expect(result.css).toContain("._c{color:red}");
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("handles no patterns found", () => {
|
|
468
|
+
const css = "._a{display:flex}._b{color:red}";
|
|
469
|
+
const html = `
|
|
470
|
+
<div class="_a">Single</div>
|
|
471
|
+
<span class="_b">Other</span>
|
|
472
|
+
`;
|
|
473
|
+
const mapping = {
|
|
474
|
+
"display:flex": "_a",
|
|
475
|
+
"color:red": "_b",
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
479
|
+
|
|
480
|
+
expect(result.patterns).toHaveLength(0);
|
|
481
|
+
expect(result.css).toBe(css); // Unchanged
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("preserves pseudo-class rules", () => {
|
|
485
|
+
const css = "._a{color:blue}._a:hover{color:red}";
|
|
486
|
+
const html = `<div class="_a _b">1</div><div class="_a _b">2</div>`;
|
|
487
|
+
const mapping = {
|
|
488
|
+
"color:blue": "_a",
|
|
489
|
+
"display:flex": "_b",
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
493
|
+
|
|
494
|
+
expect(result.css).toContain(":hover");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("respects pretty option", () => {
|
|
498
|
+
const css = "._a{display:flex}._b{color:red}";
|
|
499
|
+
const html = `<div class="_a _b">1</div><div class="_a _b">2</div>`;
|
|
500
|
+
const mapping = {
|
|
501
|
+
"display:flex": "_a",
|
|
502
|
+
"color:red": "_b",
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2, pretty: true });
|
|
506
|
+
|
|
507
|
+
if (result.patterns.length > 0) {
|
|
508
|
+
expect(result.css).toContain(" { ");
|
|
509
|
+
expect(result.css).toContain(" }");
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe("optimizeHtml", () => {
|
|
515
|
+
test("replaces class combinations with merged class", () => {
|
|
516
|
+
const html = `<div class="_a _b _c">Content</div>`;
|
|
517
|
+
const mergeMap = new Map([["_a _b", "_merged"]]);
|
|
518
|
+
|
|
519
|
+
const result = optimizeHtml(html, mergeMap);
|
|
520
|
+
|
|
521
|
+
expect(result).toContain("_merged");
|
|
522
|
+
expect(result).toContain("_c"); // Remaining class
|
|
523
|
+
expect(result).not.toContain('"_a _b _c"');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("handles multiple elements", () => {
|
|
527
|
+
const html = `
|
|
528
|
+
<div class="_a _b">First</div>
|
|
529
|
+
<span class="_a _b">Second</span>
|
|
530
|
+
<p class="_c _d">Third</p>
|
|
531
|
+
`;
|
|
532
|
+
const mergeMap = new Map([["_a _b", "_merged"]]);
|
|
533
|
+
|
|
534
|
+
const result = optimizeHtml(html, mergeMap);
|
|
535
|
+
|
|
536
|
+
expect((result.match(/_merged/g) || []).length).toBe(2);
|
|
537
|
+
expect(result).toContain("_c _d"); // Unchanged
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("handles non-prefixed classes", () => {
|
|
541
|
+
const html = `<div class="_a _b regular-class">Content</div>`;
|
|
542
|
+
const mergeMap = new Map([["_a _b", "_merged"]]);
|
|
543
|
+
|
|
544
|
+
const result = optimizeHtml(html, mergeMap);
|
|
545
|
+
|
|
546
|
+
expect(result).toContain("_merged");
|
|
547
|
+
expect(result).toContain("regular-class");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("applies longer patterns first", () => {
|
|
551
|
+
const html = `<div class="_a _b _c">Content</div>`;
|
|
552
|
+
const mergeMap = new Map([
|
|
553
|
+
["_a _b", "_m1"],
|
|
554
|
+
["_a _b _c", "_m2"],
|
|
555
|
+
]);
|
|
556
|
+
|
|
557
|
+
const result = optimizeHtml(html, mergeMap);
|
|
558
|
+
|
|
559
|
+
expect(result).toContain("_m2");
|
|
560
|
+
expect(result).not.toContain("_m1");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("returns unchanged HTML when no patterns match", () => {
|
|
564
|
+
const html = `<div class="_x _y">Content</div>`;
|
|
565
|
+
const mergeMap = new Map([["_a _b", "_merged"]]);
|
|
566
|
+
|
|
567
|
+
const result = optimizeHtml(html, mergeMap);
|
|
568
|
+
|
|
569
|
+
expect(result).toBe(html);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("returns unchanged HTML with empty mergeMap", () => {
|
|
573
|
+
const html = `<div class="_a _b">Content</div>`;
|
|
574
|
+
const mergeMap = new Map<string, string>();
|
|
575
|
+
|
|
576
|
+
const result = optimizeHtml(html, mergeMap);
|
|
577
|
+
|
|
578
|
+
expect(result).toBe(html);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// =============================================================================
|
|
584
|
+
// Edge Cases and Integration Tests
|
|
585
|
+
// =============================================================================
|
|
586
|
+
|
|
587
|
+
describe("Edge cases", () => {
|
|
588
|
+
test("handles empty HTML", () => {
|
|
589
|
+
const result = optimizeCss("._a{display:flex}", "", { "display:flex": "_a" });
|
|
590
|
+
expect(result.patterns).toHaveLength(0);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("handles empty CSS", () => {
|
|
594
|
+
const result = optimizeCss("", '<div class="_a _b">Test</div>', {});
|
|
595
|
+
expect(result.css).toBe("");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("handles special characters in class names", () => {
|
|
599
|
+
const html = `<div class="_a-1 _b_2">Content</div>`;
|
|
600
|
+
const usages = extractClassUsages(html);
|
|
601
|
+
expect(usages[0].classes).toContain("_a-1");
|
|
602
|
+
expect(usages[0].classes).toContain("_b_2");
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("handles very long class lists", () => {
|
|
606
|
+
const classes = Array.from({ length: 20 }, (_, i) => `_c${i}`);
|
|
607
|
+
const html = `<div class="${classes.join(" ")}">Content</div>`;
|
|
608
|
+
const usages = extractClassUsages(html);
|
|
609
|
+
|
|
610
|
+
expect(usages[0].classes.length).toBe(20);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("handles duplicate classes in single element", () => {
|
|
614
|
+
const html = `<div class="_a _b _a">Content</div>`;
|
|
615
|
+
const usages = extractClassUsages(html);
|
|
616
|
+
|
|
617
|
+
// Should contain 3 total classes but sorted
|
|
618
|
+
expect(usages[0].classes).toContain("_a");
|
|
619
|
+
expect(usages[0].classes).toContain("_b");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("conflicting patterns are handled correctly", () => {
|
|
623
|
+
// When _a, _b, _c all appear together multiple times
|
|
624
|
+
// AND _a, _b appear together multiple times
|
|
625
|
+
// Only one should be used (no overlap)
|
|
626
|
+
const html = `
|
|
627
|
+
<div class="_a _b _c">1</div>
|
|
628
|
+
<div class="_a _b _c">2</div>
|
|
629
|
+
<div class="_a _b">3</div>
|
|
630
|
+
<div class="_a _b">4</div>
|
|
631
|
+
`;
|
|
632
|
+
const css = "._a{a:1}._b{b:2}._c{c:3}";
|
|
633
|
+
const mapping = { "a:1": "_a", "b:2": "_b", "c:3": "_c" };
|
|
634
|
+
|
|
635
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2, maxPatternSize: 3 });
|
|
636
|
+
|
|
637
|
+
// Each class should only be used in at most one pattern
|
|
638
|
+
const usedClasses = new Set<string>();
|
|
639
|
+
for (const pattern of result.patterns) {
|
|
640
|
+
for (const cls of pattern.originalClasses) {
|
|
641
|
+
expect(usedClasses.has(cls)).toBe(false);
|
|
642
|
+
usedClasses.add(cls);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
});
|