@letsrunit/playwright 0.7.1 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letsrunit/playwright",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Playwright extensions and utilities for letsrunit",
5
5
  "keywords": [
6
6
  "testing",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "packageManager": "yarn@4.10.3",
44
44
  "dependencies": {
45
- "@letsrunit/utils": "0.7.1",
45
+ "@letsrunit/utils": "0.8.0",
46
46
  "@playwright/test": "^1.57.0",
47
47
  "case": "^1.6.3",
48
48
  "diff": "^8.0.3",
package/src/index.ts CHANGED
@@ -11,3 +11,4 @@ export * from './types';
11
11
  export * from './utils/date';
12
12
  export * from './wait';
13
13
  export * from './scrub-html';
14
+ export * from './unified-html-diff';
package/src/scrub-html.ts CHANGED
@@ -31,6 +31,9 @@ export type ScrubHtmlOptions = {
31
31
  replaceBrInHeadings?: boolean;
32
32
  /** Limit lists to max items: -1 mean no limit. Default: -1 */
33
33
  limitLists?: number;
34
+ /** Strip utility-framework classes (Tailwind, Bootstrap, UnoCSS, Windi) from class
35
+ * attributes. Removes the attribute entirely when all classes are stripped. Default: false */
36
+ dropUtilityClasses?: boolean;
34
37
  };
35
38
 
36
39
  const HTML_MIN_ATTR_THRESHOLD = 250_000; // ~70k tokens
@@ -48,6 +51,7 @@ function getDefaults(contentLength: number): Required<ScrubHtmlOptions> {
48
51
  dropComments: true,
49
52
  replaceBrInHeadings: true,
50
53
  limitLists: contentLength >= HTML_LIMIT_LISTS_THRESHOLD ? 20 : -1,
54
+ dropUtilityClasses: false,
51
55
  };
52
56
  }
53
57
 
@@ -183,6 +187,7 @@ export async function realScrubHtml(
183
187
  if (o.dropComments) dropHtmlComments(doc);
184
188
  if (o.replaceBrInHeadings) replaceBrsInHeadings(doc);
185
189
  if (o.limitLists >= 0) limitListsAndRows(doc, o.limitLists);
190
+ if (o.dropUtilityClasses) stripUtilityClasses(doc);
186
191
  if (o.normalizeWhitespace) normalizeWhitespace(doc.body);
187
192
 
188
193
  return doc.body.innerHTML;
@@ -314,6 +319,41 @@ function replaceBrsInHeadings(doc: Document) {
314
319
  });
315
320
  }
316
321
 
322
+ // ---- Utility class detection ----
323
+
324
+ // Any class containing ':' is a Tailwind/Windi/UnoCSS variant prefix (hover:, sm:, dark:focus:, …)
325
+ const UTILITY_VARIANT_RE = /:/;
326
+
327
+ // Prefix-based utility patterns (Tailwind + Bootstrap)
328
+ const UTILITY_PREFIX_RE = /^-?(?:p[xytblrse]?|m[xytblrse]?|gap|space-[xy]|w|h|min-w|min-h|max-w|max-h|size|basis|inset|top|right|bottom|left|start|end|z|text|bg|border|ring|shadow|outline|fill|stroke|divide|accent|caret|from|via|to|decoration|font|leading|tracking|indent|line-clamp|columns|aspect|object|opacity|rotate|scale|translate|skew|transition|duration|ease|delay|animate|rounded|overflow|overscroll|scroll|snap|touch|cursor|pointer-events|select|resize|flex|grid|col|row|order|auto-cols|auto-rows|items|justify|content|self|place|float|clear|list|whitespace|break|hyphens|mix-blend|bg-blend|backdrop|d|g|fs|fw|lh|align|position)-/i;
329
+
330
+ // Standalone keywords that are utilities on their own (no suffix)
331
+ const UTILITY_STANDALONE = new Set([
332
+ 'flex', 'grid', 'block', 'hidden', 'inline', 'inline-block', 'inline-flex', 'inline-grid',
333
+ 'contents', 'flow-root', 'list-item', 'table', 'container', 'truncate',
334
+ 'grow', 'shrink', 'static', 'relative', 'absolute', 'fixed', 'sticky',
335
+ 'visible', 'invisible', 'collapse', 'isolate',
336
+ 'underline', 'overline', 'line-through', 'no-underline',
337
+ 'uppercase', 'lowercase', 'capitalize', 'normal-case',
338
+ 'italic', 'not-italic', 'antialiased', 'subpixel-antialiased',
339
+ 'sr-only', 'not-sr-only', 'clearfix', 'row', 'col',
340
+ ]);
341
+
342
+ function isUtilityClass(token: string): boolean {
343
+ if (UTILITY_VARIANT_RE.test(token)) return true;
344
+ const base = token.startsWith('-') ? token.slice(1) : token;
345
+ if (UTILITY_STANDALONE.has(base)) return true;
346
+ return UTILITY_PREFIX_RE.test(token);
347
+ }
348
+
349
+ function stripUtilityClasses(doc: Document) {
350
+ for (const el of doc.body.querySelectorAll<HTMLElement>('[class]')) {
351
+ const kept = el.className.split(/\s+/).filter((t) => t && !isUtilityClass(t));
352
+ if (kept.length === 0) el.removeAttribute('class');
353
+ else el.className = kept.join(' ');
354
+ }
355
+ }
356
+
317
357
  function limitListsAndRows(doc: Document, limit: number) {
318
358
  // lists
319
359
  doc.querySelectorAll('ul, ol').forEach((list) => {
package/src/snapshot.ts CHANGED
@@ -1,16 +1,30 @@
1
1
  import { sleep } from '@letsrunit/utils';
2
2
  import type { Page } from '@playwright/test';
3
3
  import { screenshot } from './screenshot';
4
+ import { realScrubHtml } from './scrub-html';
4
5
  import type { Snapshot } from './types';
5
6
  import { waitForDomIdle } from './wait';
6
7
 
7
- export async function snapshot(page: Page): Promise<Snapshot> {
8
+ export type SnapshotOptions = {
9
+ /** Strip utility-framework classes (Tailwind, Bootstrap, UnoCSS, Windi) from the captured HTML. */
10
+ dropUtilityClasses?: boolean;
11
+ };
12
+
13
+ export async function snapshot(page: Page, opts: SnapshotOptions = {}): Promise<Snapshot> {
8
14
  await sleep(500);
9
15
  await waitForDomIdle(page);
10
16
 
11
17
  const [url, html, file] = await Promise.all([page.url(), getContentWithMarkedHidden(page), screenshot(page)]);
12
18
 
13
- return { url, html, screenshot: file };
19
+ const finalHtml = opts.dropUtilityClasses
20
+ ? await realScrubHtml({ html, url }, {
21
+ dropHidden: false, dropHead: false, dropSvg: false, pickMain: false,
22
+ stripAttributes: 0, normalizeWhitespace: false, dropComments: false,
23
+ replaceBrInHeadings: false, limitLists: -1, dropUtilityClasses: true,
24
+ })
25
+ : html;
26
+
27
+ return { url, html: finalHtml, screenshot: file };
14
28
  }
15
29
 
16
30
  async function getContentWithMarkedHidden(page: Page): Promise<string> {