@cleaver/breakcheck-core 0.1.0

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.
Files changed (59) hide show
  1. package/README.md +152 -0
  2. package/dist/__tests__/compare/index.test.js +322 -0
  3. package/dist/__tests__/compare/index.test.js.map +1 -0
  4. package/dist/__tests__/rules/RulesDsl.test.js +190 -0
  5. package/dist/__tests__/rules/RulesDsl.test.js.map +1 -0
  6. package/dist/__tests__/rules/RulesEngine.test.js +142 -0
  7. package/dist/__tests__/rules/RulesEngine.test.js.map +1 -0
  8. package/dist/__tests__/snapshot/index.test.js +238 -0
  9. package/dist/__tests__/snapshot/index.test.js.map +1 -0
  10. package/dist/__tests__/view/index.test.js +102 -0
  11. package/dist/__tests__/view/index.test.js.map +1 -0
  12. package/dist/api/index.js +69 -0
  13. package/dist/api/index.js.map +1 -0
  14. package/dist/core/compare/classes/ComparisonRepository.js +77 -0
  15. package/dist/core/compare/classes/ComparisonRepository.js.map +1 -0
  16. package/dist/core/compare/index.js +81 -0
  17. package/dist/core/compare/index.js.map +1 -0
  18. package/dist/core/crawler/implementations/cheerio.js +46 -0
  19. package/dist/core/crawler/implementations/cheerio.js.map +1 -0
  20. package/dist/core/crawler/implementations/playwright.js +44 -0
  21. package/dist/core/crawler/implementations/playwright.js.map +1 -0
  22. package/dist/core/crawler/index.js +61 -0
  23. package/dist/core/crawler/index.js.map +1 -0
  24. package/dist/core/rules/RulesDsl.js +251 -0
  25. package/dist/core/rules/RulesDsl.js.map +1 -0
  26. package/dist/core/rules/RulesEngine.js +91 -0
  27. package/dist/core/rules/RulesEngine.js.map +1 -0
  28. package/dist/core/snapshot/classes/LoadedSnapshot.js +37 -0
  29. package/dist/core/snapshot/classes/LoadedSnapshot.js.map +1 -0
  30. package/dist/core/snapshot/classes/SnapshotRepository.js +192 -0
  31. package/dist/core/snapshot/classes/SnapshotRepository.js.map +1 -0
  32. package/dist/core/snapshot/index.js +67 -0
  33. package/dist/core/snapshot/index.js.map +1 -0
  34. package/dist/core/view/index.handlers.js +70 -0
  35. package/dist/core/view/index.handlers.js.map +1 -0
  36. package/dist/core/view/index.js +50 -0
  37. package/dist/core/view/index.js.map +1 -0
  38. package/dist/index.d.ts +14 -0
  39. package/dist/index.js +13 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lib/logger.js +26 -0
  42. package/dist/lib/logger.js.map +1 -0
  43. package/dist/lib/root.js +25 -0
  44. package/dist/lib/root.js.map +1 -0
  45. package/dist/public/css/diff2html.min.css +1 -0
  46. package/dist/public/js/diff2html.min.js +1 -0
  47. package/dist/types/api.js +3 -0
  48. package/dist/types/api.js.map +1 -0
  49. package/dist/types/compare.js +3 -0
  50. package/dist/types/compare.js.map +1 -0
  51. package/dist/types/crawler.js +2 -0
  52. package/dist/types/crawler.js.map +1 -0
  53. package/dist/types/rules.js +6 -0
  54. package/dist/types/rules.js.map +1 -0
  55. package/dist/types/snapshot.js +3 -0
  56. package/dist/types/snapshot.js.map +1 -0
  57. package/dist/views/diff.ejs +59 -0
  58. package/dist/views/index.ejs +83 -0
  59. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # Breakcheck Core
2
+
3
+ This package provides the core programmatic API for **Breakcheck**. Use this library to integrate website snapshotting, comparison, and viewing directly into your own JavaScript or TypeScript projects.
4
+
5
+ This package is intended for developers who need to build custom automation or tooling. If you're looking to use Breakcheck from the command line, see the main [`breakcheck`](https://github.com/cleaver/breakcheck) package.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install breakcheck-core
13
+ ```
14
+
15
+ ---
16
+
17
+ ## API Usage
18
+
19
+ The `breakcheck-core` library exposes a set of simple, asynchronous functions to manage the entire workflow.
20
+
21
+ ### Creating a Snapshot
22
+
23
+ To begin, you need to create a "snapshot" of a website. A snapshot is a record of a site's state at a specific point in time, created by crawling the site and saving its content.
24
+
25
+ The `createSnapshotFromConfig` function takes a configuration object and returns the results of the crawl.
26
+
27
+ ```javascript
28
+ import { createSnapshotFromConfig } from "breakcheck-core";
29
+
30
+ async function takeSnapshot() {
31
+ const snapshotConfig = {
32
+ // The URL where the crawler will start
33
+ baseUrl: "http://localhost:3000",
34
+
35
+ // A unique name for this snapshot
36
+ name: "my-site-before-changes",
37
+
38
+ // Settings for the crawler
39
+ crawlSettings: {
40
+ baseUrl: "http://localhost:3000",
41
+ // 'cheerio' for static sites, 'playwright' for dynamic, JS-heavy sites
42
+ crawlerType: "cheerio",
43
+ maxDepth: 3, // How many links deep to follow
44
+ },
45
+
46
+ // Optional: Write a list of all crawled URLs to a file
47
+ urlListPath: "./crawled-urls.txt",
48
+ };
49
+
50
+ const result = await createSnapshotFromConfig(snapshotConfig);
51
+
52
+ if (result.status === "success") {
53
+ console.log(`✅ Snapshot created: ${result.snapshotId}`);
54
+ console.log(`📊 Pages crawled: ${result.pageCount}`);
55
+ } else {
56
+ console.error(`❌ Snapshot failed: ${result.message}`);
57
+ }
58
+ }
59
+
60
+ takeSnapshot();
61
+ ```
62
+
63
+ ### Listing Snapshots
64
+
65
+ You can get a list of all locally saved snapshots using the `listSnapshots` function.
66
+
67
+ ```javascript
68
+ import { listSnapshots } from "breakcheck-core";
69
+
70
+ async function showSnapshots() {
71
+ const snapshots = await listSnapshots();
72
+
73
+ if (snapshots.length === 0) {
74
+ console.log("No snapshots found.");
75
+ return;
76
+ }
77
+
78
+ console.log("Available Snapshots:");
79
+ snapshots.forEach((snapshot) => {
80
+ console.log(
81
+ `- ${snapshot.name} (${snapshot.pageCount} pages, created on ${snapshot.date})`
82
+ );
83
+ });
84
+ }
85
+
86
+ showSnapshots();
87
+ ```
88
+
89
+ ### Running a Comparison
90
+
91
+ The core of the tool is comparing two snapshots. The `runComparison` function compares a "before" and "after" snapshot, applies rules to ignore dynamic content, and saves a detailed diff report.
92
+
93
+ ```javascript
94
+ import { runComparison } from "breakcheck-core";
95
+
96
+ async function compareStates() {
97
+ const comparisonConfig = {
98
+ // The name of the 'before' snapshot
99
+ beforeSnapshotId: "my-site-before-changes",
100
+
101
+ // The name of the 'after' snapshot
102
+ afterSnapshotId: "my-site-after-changes",
103
+
104
+ // A unique name for the comparison results directory
105
+ comparisonName: "v1-vs-v2-comparison",
106
+
107
+ // The ruleset to apply. 'default' uses built-in baseline rules.
108
+ // You can also provide a path to a rules file or a ruleset object.
109
+ ruleset: "default",
110
+ };
111
+
112
+ const summary = await runComparison(comparisonConfig);
113
+
114
+ if (summary.status === "completed") {
115
+ console.log(`✅ Comparison complete!`);
116
+ console.log(` - Overall result: ${summary.overallResult.toUpperCase()}`);
117
+ console.log(` - Pages with differences: ${summary.pagesWithDifferences}`);
118
+ console.log(` - Results saved to: ${summary.resultsPath}`);
119
+ } else {
120
+ console.error("❌ Comparison failed.");
121
+ }
122
+ }
123
+
124
+ compareStates();
125
+ ```
126
+
127
+ ### Viewing Comparison Results
128
+
129
+ After a comparison is complete, you can launch a local web server to view the results in your browser. The `startViewServer` function starts an Express server that renders the diffs.
130
+
131
+ ```javascript
132
+ import { startViewServer } from "breakcheck-core";
133
+
134
+ async function viewResults() {
135
+ const comparisonName = "v1-vs-v2-comparison"; // The name of the comparison to view
136
+ const port = 8080;
137
+
138
+ try {
139
+ const server = await startViewServer(comparisonName, port);
140
+ console.log(`🌐 View server running at http://localhost:${port}`);
141
+ console.log("Press Ctrl+C to stop the server.");
142
+
143
+ // You can add logic to automatically open the browser or close the server
144
+ // For example, to stop it after 60 seconds:
145
+ // setTimeout(() => server.close(), 60000);
146
+ } catch (error) {
147
+ console.error(`❌ Could not start view server: ${error.message}`);
148
+ }
149
+ }
150
+
151
+ viewResults();
152
+ ```
@@ -0,0 +1,322 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { comparePage, compareSnapshots } from "../../core/compare";
3
+ import { ComparisonRepository } from "../../core/compare/classes/ComparisonRepository";
4
+ /**
5
+ * Creates a mock SnapshotRepository for testing
6
+ */
7
+ class MockSnapshotRepository {
8
+ beforeSnapshot;
9
+ afterSnapshot;
10
+ constructor(beforeSnapshot, afterSnapshot) {
11
+ this.beforeSnapshot = beforeSnapshot;
12
+ this.afterSnapshot = afterSnapshot;
13
+ }
14
+ async loadSnapshot(name) {
15
+ return name === "before" ? this.beforeSnapshot : this.afterSnapshot;
16
+ }
17
+ async saveSnapshot(name, data) {
18
+ return 0;
19
+ }
20
+ async generateUrlList(name) {
21
+ return "";
22
+ }
23
+ async listSnapshots() {
24
+ return [];
25
+ }
26
+ }
27
+ /**
28
+ * Creates a mock RulesEngine for testing
29
+ */
30
+ class MockRulesEngine {
31
+ constructor(rulesetOrName) {
32
+ // No-op constructor
33
+ }
34
+ process(html) {
35
+ return html;
36
+ }
37
+ }
38
+ /**
39
+ * Creates a mock ComparisonRepository for testing
40
+ */
41
+ class MockComparisonRepository {
42
+ index = {
43
+ urls: {},
44
+ metadata: {
45
+ timestamp: new Date().toISOString(),
46
+ totalPages: 0,
47
+ pagesWithDifferences: 0,
48
+ },
49
+ };
50
+ constructor() { }
51
+ static async create(name, metadata) {
52
+ const instance = new MockComparisonRepository();
53
+ instance.index.metadata = {
54
+ ...metadata,
55
+ timestamp: new Date().toISOString(),
56
+ totalPages: 0,
57
+ pagesWithDifferences: 0,
58
+ };
59
+ return instance;
60
+ }
61
+ async savePageDiff(pageDiff) {
62
+ this.index.urls[pageDiff.url] = {
63
+ filename: `${pageDiff.url}.json.gz`,
64
+ hasDifferences: pageDiff.hasDifferences,
65
+ };
66
+ this.index.metadata.totalPages++;
67
+ if (pageDiff.hasDifferences) {
68
+ this.index.metadata.pagesWithDifferences++;
69
+ }
70
+ }
71
+ async finalizeComparison() {
72
+ return "/mock/comparison/path";
73
+ }
74
+ getIndex() {
75
+ return this.index;
76
+ }
77
+ }
78
+ async function createMockSnapshotRepository(beforeSnapshot, afterSnapshot) {
79
+ return new MockSnapshotRepository(beforeSnapshot, afterSnapshot);
80
+ }
81
+ function createMockRulesEngine() {
82
+ return new MockRulesEngine("default");
83
+ }
84
+ function createMockSnapshot(urls, content) {
85
+ return {
86
+ index: {
87
+ urls,
88
+ metadata: {
89
+ baseUrl: "https://example.com",
90
+ timestamp: new Date().toISOString(),
91
+ totalPages: Object.keys(urls).length,
92
+ },
93
+ },
94
+ getPage: async (url) => ({
95
+ url,
96
+ finalUrl: url,
97
+ content,
98
+ statusCode: 200,
99
+ headers: { "content-type": "text/html" },
100
+ }),
101
+ };
102
+ }
103
+ describe("Compare Functions", () => {
104
+ describe("comparePage", () => {
105
+ it("should detect content differences between snapshots", async () => {
106
+ const before = {
107
+ url: "https://example.com",
108
+ finalUrl: "https://example.com",
109
+ content: "<html>\n<body>Hello World</body>\n</html>",
110
+ statusCode: 200,
111
+ headers: { "content-type": "text/html" },
112
+ };
113
+ const after = {
114
+ url: "https://example.com",
115
+ finalUrl: "https://example.com",
116
+ content: "<html>\n<body>Hello New World</body>\n</html>",
117
+ statusCode: 200,
118
+ headers: { "content-type": "text/html" },
119
+ };
120
+ const result = await comparePage(before, after);
121
+ expect(result.url).toBe("https://example.com");
122
+ expect(result.hasDifferences).toBe(true);
123
+ // Expect 4 lines: One addition, one removal, two unchanged
124
+ expect(result.differences).toHaveLength(4);
125
+ const numberLinesAdded = result.differences.filter((d) => d.added).length;
126
+ const numberLinesRemoved = result.differences.filter((d) => d.removed).length;
127
+ const numberLinesUnchanged = result.differences.filter((d) => !d.added && !d.removed).length;
128
+ expect(numberLinesAdded).toBe(1);
129
+ expect(numberLinesRemoved).toBe(1);
130
+ expect(numberLinesUnchanged).toBe(2);
131
+ });
132
+ it("should return no differences for identical snapshots", async () => {
133
+ const snapshot = {
134
+ url: "https://example.com",
135
+ finalUrl: "https://example.com",
136
+ content: "<html><body>Hello World</body></html>",
137
+ statusCode: 200,
138
+ headers: { "content-type": "text/html" },
139
+ };
140
+ const result = await comparePage(snapshot, snapshot);
141
+ expect(result.url).toBe("https://example.com");
142
+ expect(result.hasDifferences).toBe(false);
143
+ expect(result.differences).toHaveLength(1);
144
+ expect(result.differences[0].added).toBeUndefined();
145
+ expect(result.differences[0].removed).toBeUndefined();
146
+ });
147
+ });
148
+ describe("compareSnapshots", () => {
149
+ async function createSetup(beforeUrls, afterUrls, beforeContent, afterContent) {
150
+ const beforeSnapshot = createMockSnapshot(beforeUrls, beforeContent);
151
+ const afterSnapshot = createMockSnapshot(afterUrls, afterContent);
152
+ const snapshotRepository = await createMockSnapshotRepository(beforeSnapshot, afterSnapshot);
153
+ const comparisonRepository = await ComparisonRepository.create("test-comparison", {
154
+ beforeSnapshotId: "before",
155
+ afterSnapshotId: "after",
156
+ rulesUsedIdentifier: "default",
157
+ });
158
+ const rulesEngine = createMockRulesEngine();
159
+ return {
160
+ snapshotRepository,
161
+ comparisonRepository,
162
+ rulesEngine,
163
+ comparisonParams: {
164
+ beforeSnapshotId: "before",
165
+ afterSnapshotId: "after",
166
+ comparisonName: "test-comparison",
167
+ ruleset: "default",
168
+ },
169
+ };
170
+ }
171
+ it("should detect content differences between snapshots", async () => {
172
+ const beforeUrls = {
173
+ "https://example.com": {
174
+ filename: "example.html",
175
+ statusCode: 200,
176
+ },
177
+ };
178
+ const afterUrls = {
179
+ "https://example.com": {
180
+ filename: "example.html",
181
+ statusCode: 200,
182
+ },
183
+ };
184
+ const beforeContent = "<html>\n<body>Hello World</body>\n</html>";
185
+ const afterContent = "<html>\n<body>Hello New World</body>\n</html>";
186
+ const setup = await createSetup(beforeUrls, afterUrls, beforeContent, afterContent);
187
+ const result = await compareSnapshots(setup.comparisonParams, setup.snapshotRepository, setup.comparisonRepository, setup.rulesEngine);
188
+ expect(result.status).toBe("completed");
189
+ expect(result.overallResult).toBe("fail");
190
+ expect(result.totalPagesCompared).toBe(1);
191
+ expect(result.pagesWithDifferences).toBe(1);
192
+ expect(result.newUrls).toHaveLength(0);
193
+ expect(result.removedUrls).toHaveLength(0);
194
+ });
195
+ it("should detect new and removed URLs between snapshots", async () => {
196
+ const beforeUrls = {
197
+ "https://example.com/old": {
198
+ filename: "old.html",
199
+ statusCode: 200,
200
+ },
201
+ };
202
+ const afterUrls = {
203
+ "https://example.com/new": {
204
+ filename: "new.html",
205
+ statusCode: 200,
206
+ },
207
+ };
208
+ const beforeContent = "<html><body>Old Page</body></html>";
209
+ const afterContent = "<html><body>New Page</body></html>";
210
+ const setup = await createSetup(beforeUrls, afterUrls, beforeContent, afterContent);
211
+ const result = await compareSnapshots(setup.comparisonParams, setup.snapshotRepository, setup.comparisonRepository, setup.rulesEngine);
212
+ expect(result.status).toBe("completed");
213
+ expect(result.overallResult).toBe("pass");
214
+ expect(result.totalPagesCompared).toBe(0);
215
+ expect(result.pagesWithDifferences).toBe(0);
216
+ expect(result.newUrls).toHaveLength(1);
217
+ expect(result.newUrls[0]).toBe("https://example.com/new");
218
+ expect(result.removedUrls).toHaveLength(1);
219
+ expect(result.removedUrls[0]).toBe("https://example.com/old");
220
+ });
221
+ it("should filter URLs when specified", async () => {
222
+ const beforeUrls = {
223
+ "https://example.com/page1": {
224
+ filename: "page1.html",
225
+ statusCode: 200,
226
+ },
227
+ "https://example.com/page2": {
228
+ filename: "page2.html",
229
+ statusCode: 200,
230
+ },
231
+ };
232
+ const afterUrls = {
233
+ "https://example.com/page1": {
234
+ filename: "page1.html",
235
+ statusCode: 200,
236
+ },
237
+ "https://example.com/page2": {
238
+ filename: "page2.html",
239
+ statusCode: 200,
240
+ },
241
+ };
242
+ const beforeContent = "<html><body>Original Content</body></html>";
243
+ const afterContent = "<html><body>Modified Content</body></html>";
244
+ const setup = await createSetup(beforeUrls, afterUrls, beforeContent, afterContent);
245
+ const result = await compareSnapshots({
246
+ ...setup.comparisonParams,
247
+ urls: ["https://example.com/page1"],
248
+ }, setup.snapshotRepository, setup.comparisonRepository, setup.rulesEngine);
249
+ expect(result.status).toBe("completed");
250
+ expect(result.overallResult).toBe("fail");
251
+ expect(result.totalPagesCompared).toBe(1);
252
+ expect(result.pagesWithDifferences).toBe(1);
253
+ expect(result.newUrls).toHaveLength(0);
254
+ expect(result.removedUrls).toHaveLength(0);
255
+ });
256
+ it("should return no differences for identical snapshots", async () => {
257
+ const beforeUrls = {
258
+ "https://example.com": {
259
+ filename: "example.html",
260
+ statusCode: 200,
261
+ },
262
+ };
263
+ const afterUrls = {
264
+ "https://example.com": {
265
+ filename: "example.html",
266
+ statusCode: 200,
267
+ },
268
+ };
269
+ const beforeContent = "<html><body>Hello World</body></html>";
270
+ const afterContent = "<html><body>Hello World</body></html>";
271
+ const setup = await createSetup(beforeUrls, afterUrls, beforeContent, afterContent);
272
+ const result = await compareSnapshots(setup.comparisonParams, setup.snapshotRepository, setup.comparisonRepository, setup.rulesEngine);
273
+ expect(result.status).toBe("completed");
274
+ expect(result.overallResult).toBe("pass");
275
+ expect(result.totalPagesCompared).toBe(1);
276
+ expect(result.pagesWithDifferences).toBe(0);
277
+ expect(result.newUrls).toHaveLength(0);
278
+ expect(result.removedUrls).toHaveLength(0);
279
+ });
280
+ it("should return URLs in ascending sorted order regardless of input order", async () => {
281
+ const beforeUrls = {
282
+ "https://example.com/m": {
283
+ filename: "m.html",
284
+ statusCode: 200,
285
+ },
286
+ "https://example.com/a": {
287
+ filename: "a.html",
288
+ statusCode: 200,
289
+ },
290
+ "https://example.com/z": {
291
+ filename: "z.html",
292
+ statusCode: 200,
293
+ },
294
+ };
295
+ const afterUrls = {
296
+ "https://example.com/m": {
297
+ filename: "m.html",
298
+ statusCode: 200,
299
+ },
300
+ "https://example.com/a": {
301
+ filename: "a.html",
302
+ statusCode: 200,
303
+ },
304
+ "https://example.com/z": {
305
+ filename: "z.html",
306
+ statusCode: 200,
307
+ },
308
+ };
309
+ const beforeContent = "<html><body>Original Content</body></html>";
310
+ const afterContent = "<html><body>Modified Content</body></html>";
311
+ const setup = await createSetup(beforeUrls, afterUrls, beforeContent, afterContent);
312
+ const result = await compareSnapshots(setup.comparisonParams, setup.snapshotRepository, setup.comparisonRepository, setup.rulesEngine);
313
+ expect(result.status).toBe("completed");
314
+ expect(result.overallResult).toBe("fail");
315
+ expect(result.totalPagesCompared).toBe(3);
316
+ expect(result.pagesWithDifferences).toBe(3);
317
+ expect(result.newUrls).toHaveLength(0);
318
+ expect(result.removedUrls).toHaveLength(0);
319
+ });
320
+ });
321
+ });
322
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../../src/__tests__/compare/index.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,190 @@
1
+ import { readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { processRulesDsl } from "../../core/rules/RulesDsl";
5
+ // Mock fs and path modules
6
+ vi.mock("fs", () => ({
7
+ readFileSync: vi.fn(),
8
+ }));
9
+ vi.mock("path", () => ({
10
+ join: vi.fn(),
11
+ }));
12
+ // Mock the findRootDir function to return a predictable path
13
+ vi.mock("../../lib/root", () => ({
14
+ findRootDir: vi.fn().mockResolvedValue("/mock/root"),
15
+ }));
16
+ describe("RulesDsl", () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ join.mockImplementation((...args) => args.join("/"));
20
+ });
21
+ afterEach(() => {
22
+ vi.resetAllMocks();
23
+ });
24
+ describe("processRulesDsl", () => {
25
+ it("should throw error if rules file not found", async () => {
26
+ readFileSync.mockImplementation(() => {
27
+ throw new Error("File not found");
28
+ });
29
+ await expect(processRulesDsl("non-existent")).rejects.toThrow("Rules file not found: rules/non-existent/rules.breakcheck");
30
+ });
31
+ it("should parse single action rules", async () => {
32
+ const rulesContent = `
33
+ css:.ad-container do: exclude
34
+ css:#session-id do: exclude
35
+ css:.important-note do: include content_regex:"Warning:"
36
+ `;
37
+ readFileSync.mockReturnValue(rulesContent);
38
+ const result = await processRulesDsl("test-rules");
39
+ expect(result).toEqual({
40
+ name: "test-rules",
41
+ rules: [
42
+ {
43
+ selector: ".ad-container",
44
+ actions: [{ action: "exclude", modifiers: {} }],
45
+ },
46
+ {
47
+ selector: "#session-id",
48
+ actions: [{ action: "exclude", modifiers: {} }],
49
+ },
50
+ {
51
+ selector: ".important-note",
52
+ actions: [
53
+ {
54
+ action: "include",
55
+ modifiers: { content_regex: "Warning:" },
56
+ },
57
+ ],
58
+ },
59
+ ],
60
+ });
61
+ });
62
+ it("should parse action blocks", async () => {
63
+ const rulesContent = `
64
+ css:img do
65
+ remove_attr attr:"srcset"
66
+ remove_attr attr:"sizes"
67
+ rewrite_attr attr:"src" regex:"//cdn\\d+\\.example\\.com/" replace:"//cdn.example.com/"
68
+ end
69
+ `;
70
+ readFileSync.mockReturnValue(rulesContent);
71
+ const result = await processRulesDsl("test-rules");
72
+ expect(result).toEqual({
73
+ name: "test-rules",
74
+ rules: [
75
+ {
76
+ selector: "img",
77
+ actions: [
78
+ { action: "remove_attr", modifiers: { attr: "srcset" } },
79
+ { action: "remove_attr", modifiers: { attr: "sizes" } },
80
+ {
81
+ action: "rewrite_attr",
82
+ modifiers: {
83
+ attr: "src",
84
+ regex: "//cdn\\d+\\.example\\.com/",
85
+ replace: "//cdn.example.com/",
86
+ },
87
+ },
88
+ ],
89
+ },
90
+ ],
91
+ });
92
+ });
93
+ it("should parse rewrite content rules", async () => {
94
+ const rulesContent = `
95
+ css:.timestamp do: rewrite_content regex:"\\d{2}/\\d{2}/\\d{4}" replace:"DATE_STAMP"
96
+ css:.view-count do: rewrite_content regex:"\\d{1,3}(,\\d{3})* views" replace:"VIEW_COUNT views"
97
+ `;
98
+ readFileSync.mockReturnValue(rulesContent);
99
+ const result = await processRulesDsl("test-rules");
100
+ expect(result).toEqual({
101
+ name: "test-rules",
102
+ rules: [
103
+ {
104
+ selector: ".timestamp",
105
+ actions: [
106
+ {
107
+ action: "rewrite_content",
108
+ modifiers: {
109
+ regex: "\\d{2}/\\d{2}/\\d{4}",
110
+ replace: "DATE_STAMP",
111
+ },
112
+ },
113
+ ],
114
+ },
115
+ {
116
+ selector: ".view-count",
117
+ actions: [
118
+ {
119
+ action: "rewrite_content",
120
+ modifiers: {
121
+ regex: "\\d{1,3}(,\\d{3})* views",
122
+ replace: "VIEW_COUNT views",
123
+ },
124
+ },
125
+ ],
126
+ },
127
+ ],
128
+ });
129
+ });
130
+ it("should handle comments and whitespace", async () => {
131
+ const rulesContent = `
132
+ -- This is a comment
133
+ css:.ad-container do: exclude
134
+
135
+ -- Another comment
136
+ css:.important-note do: include content_regex:"Warning:"
137
+ `;
138
+ readFileSync.mockReturnValue(rulesContent);
139
+ const result = await processRulesDsl("test-rules");
140
+ expect(result).toEqual({
141
+ name: "test-rules",
142
+ rules: [
143
+ {
144
+ selector: ".ad-container",
145
+ actions: [{ action: "exclude", modifiers: {} }],
146
+ },
147
+ {
148
+ selector: ".important-note",
149
+ actions: [
150
+ {
151
+ action: "include",
152
+ modifiers: { content_regex: "Warning:" },
153
+ },
154
+ ],
155
+ },
156
+ ],
157
+ });
158
+ });
159
+ it("should throw error for invalid syntax", async () => {
160
+ const rulesContent = `
161
+ css:.ad-container do exclude # Missing colon after do
162
+ `;
163
+ readFileSync.mockReturnValue(rulesContent);
164
+ await expect(processRulesDsl("test-rules")).rejects.toThrow("Syntax errors in rules file");
165
+ });
166
+ it("should throw error for invalid action", async () => {
167
+ const rulesContent = `
168
+ css:.ad-container do: invalid_action
169
+ `;
170
+ readFileSync.mockReturnValue(rulesContent);
171
+ await expect(processRulesDsl("test-rules")).rejects.toThrow("Syntax errors in rules file");
172
+ });
173
+ it("should throw error for unclosed action block", async () => {
174
+ const rulesContent = `
175
+ css:img do
176
+ remove_attr attr:"srcset"
177
+ `;
178
+ readFileSync.mockReturnValue(rulesContent);
179
+ await expect(processRulesDsl("test-rules")).rejects.toThrow("Syntax errors in rules file");
180
+ });
181
+ it("should throw error for missing required modifiers", async () => {
182
+ const rulesContent = `
183
+ css:img do: remove_attr
184
+ `;
185
+ readFileSync.mockReturnValue(rulesContent);
186
+ await expect(processRulesDsl("test-rules")).rejects.toThrow("Syntax errors in rules file");
187
+ });
188
+ });
189
+ });
190
+ //# sourceMappingURL=RulesDsl.test.js.map