@d-zero/archaeologist 2.0.0 → 3.0.1

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/README.md CHANGED
@@ -12,16 +12,22 @@
12
12
  ## CLI
13
13
 
14
14
  ```sh
15
- npx @d-zero/archaeologist -f <filepath> [--limit <number>] [--debug]
15
+ npx @d-zero/archaeologist -f <listfile> [options]
16
16
  ```
17
17
 
18
18
  URLリストを持つファイルを指定して実行します。
19
19
 
20
20
  ### オプション
21
21
 
22
- - `-f, --file <filepath>`: URLリストを持つファイルのパス(必須)
22
+ - `-f, --listfile <filepath>`: URLリストを持つファイルのパス(必須)
23
+ - `-t, --type <types>`: 比較タイプの指定(`image,dom,text`、カンマ区切り)
24
+ - `-s, --selector <selector>`: 比較対象を限定するCSSセレクター
25
+ - `-i, --ignore <selector>`: 無視するCSSセレクター
26
+ - `--devices <devices>`: デバイス指定(`desktop,mobile,tablet`、カンマ区切り)
27
+ - `--freeze <filepath>`: フリーズモード用ファイルパス
23
28
  - `--limit <number>`: 並列実行数の上限(デフォルト: 10)
24
29
  - `--debug`: デバッグモード(デフォルト: false)
30
+ - `--verbose`: 詳細ログモード(デフォルト: false)
25
31
 
26
32
  ### ファイルフォーマット
27
33
 
@@ -53,7 +59,7 @@ https://example.com/xyz/001
53
59
 
54
60
  ## ページフック
55
61
 
56
- [Frontmatter](https://jekyllrb.com/docs/front-matter/)の`hooks`に配列としてスクリプトファイルのパスを渡すと、ページを開いた後(厳密にはPuppetterの`waitUntil: 'networkidle0'`のタイミング直後)にそれらのスクリプトを実行します。スクリプトは配列の順番通りに逐次実行されます。
62
+ [Frontmatter](https://jekyllrb.com/docs/front-matter/)の`hooks`に配列としてスクリプトファイルのパスを渡すと、ページを開いた後(厳密にはPuppeteerの`waitUntil: 'networkidle0'`のタイミング直後)にそれらのスクリプトを実行します。スクリプトは配列の順番通りに逐次実行されます。
57
63
 
58
64
  ```txt
59
65
  ---
@@ -4,6 +4,9 @@ export type ChildProcessParams = {
4
4
  list: readonly URLPair[];
5
5
  dir: string;
6
6
  useOldMode: boolean;
7
- htmlDiffOnly: boolean;
7
+ types?: readonly string[];
8
+ selector?: string;
9
+ ignore?: string;
10
+ devices?: readonly string[];
8
11
  hooks?: readonly PageHook[];
9
12
  };
@@ -4,11 +4,13 @@ import { createChildProcess } from '@d-zero/puppeteer-dealer';
4
4
  import { delay } from '@d-zero/shared/delay';
5
5
  import c from 'ansi-colors';
6
6
  import { diffImages } from './modules/diff-images.js';
7
+ import { diffText } from './modules/diff-text.js';
7
8
  import { diffTree } from './modules/diff-tree.js';
8
9
  import { getData } from './modules/get-data.js';
10
+ import { normalizeTextDocument } from './modules/normalize-text-document.js';
9
11
  import { score } from './utils.js';
10
12
  createChildProcess((param) => {
11
- const { list, dir, htmlDiffOnly } = param;
13
+ const { list, dir, types = ['image', 'dom', 'text'], selector, ignore, devices, } = param;
12
14
  return {
13
15
  async eachPage({ page, url: urlA, index }, logger) {
14
16
  const urlPair = list.find(([url]) => url === urlA);
@@ -18,7 +20,10 @@ createChildProcess((param) => {
18
20
  const dataPair = [];
19
21
  for (const url of urlPair) {
20
22
  const data = await getData(page, url, {
21
- htmlDiffOnly,
23
+ htmlDiffOnly: !types.includes('image'),
24
+ selector,
25
+ ignore,
26
+ devices,
22
27
  }, logger);
23
28
  dataPair.push(data);
24
29
  await delay(600);
@@ -36,47 +41,71 @@ createChildProcess((param) => {
36
41
  if (!screenshotB) {
37
42
  throw new Error(`Failed to get screenshotB: ${id}`);
38
43
  }
39
- const imageDiff = await diffImages(screenshotA, screenshotB, (phase, data) => {
40
- switch (phase) {
41
- case 'create': {
42
- logger(`${sizeName} ${outputUrl} 🖼️ Create images`);
43
- break;
44
- }
45
- case 'resize': {
46
- const { width, height } = data;
47
- logger(`${sizeName} ${outputUrl} ↔️ Resize images to ${width}x${height}`);
48
- break;
49
- }
50
- case 'diff': {
51
- logger(`${sizeName} ${outputUrl} 📊 Compare images`);
52
- break;
44
+ let image = null;
45
+ if (types.includes('image')) {
46
+ const imageDiff = await diffImages(screenshotA, screenshotB, (phase, data) => {
47
+ switch (phase) {
48
+ case 'create': {
49
+ logger(`${sizeName} ${outputUrl} 🖼️ Create images`);
50
+ break;
51
+ }
52
+ case 'resize': {
53
+ const { width, height } = data;
54
+ logger(`${sizeName} ${outputUrl} ↔️ Resize images to ${width}x${height}`);
55
+ break;
56
+ }
57
+ case 'diff': {
58
+ logger(`${sizeName} ${outputUrl} 📊 Compare images`);
59
+ break;
60
+ }
53
61
  }
62
+ });
63
+ if (imageDiff) {
64
+ logger(`${sizeName} ${outputUrl} 🧩 Matches ${score(imageDiff.matches, 0.9)}`);
65
+ await delay(1500);
66
+ await writeFile(path.resolve(dir, `${id}_a.png`), imageDiff.images.a);
67
+ await writeFile(path.resolve(dir, `${id}_b.png`), imageDiff.images.b);
68
+ const outFilePath = path.resolve(dir, `${id}_diff.png`);
69
+ logger(`${sizeName} ${outputUrl} 📊 Save diff image to ${path.relative(dir, outFilePath)}`);
70
+ await writeFile(outFilePath, imageDiff.images.diff);
71
+ image = {
72
+ matches: imageDiff.matches,
73
+ file: outFilePath,
74
+ };
54
75
  }
55
- });
56
- let image = null;
57
- if (imageDiff) {
58
- logger(`${sizeName} ${outputUrl} 🧩 Matches ${score(imageDiff.matches, 0.9)}`);
59
- await delay(1500);
60
- await writeFile(path.resolve(dir, `${id}_a.png`), imageDiff.images.a);
61
- await writeFile(path.resolve(dir, `${id}_b.png`), imageDiff.images.b);
62
- const outFilePath = path.resolve(dir, `${id}_diff.png`);
63
- logger(`${sizeName} ${outputUrl} 📊 Save diff image to ${path.relative(dir, outFilePath)}`);
64
- await writeFile(outFilePath, imageDiff.images.diff);
65
- image = {
66
- matches: imageDiff.matches,
76
+ }
77
+ let dom = null;
78
+ if (types.includes('dom')) {
79
+ const htmlDiff = diffTree(a.url, b.url, screenshotA.domTree, screenshotB.domTree);
80
+ const outFilePath = path.resolve(dir, `${id}_html.diff`);
81
+ await writeFile(outFilePath, htmlDiff.result, { encoding: 'utf8' });
82
+ dom = {
83
+ matches: htmlDiff.matches,
84
+ diff: htmlDiff.changed ? htmlDiff.result : null,
85
+ file: outFilePath,
86
+ };
87
+ }
88
+ let text = null;
89
+ if (types.includes('text')) {
90
+ const contentA = normalizeTextDocument(screenshotA.text.textContent);
91
+ const contentB = normalizeTextDocument(screenshotB.text.textContent);
92
+ const altTextListA = screenshotA.text.altTextList.join('\n');
93
+ const altTextListB = screenshotB.text.altTextList.join('\n');
94
+ const textA = `${contentA}\n\n${altTextListA}`;
95
+ const textB = `${contentB}\n\n${altTextListB}`;
96
+ const textDiff = diffText(a.url, b.url, textA, textB);
97
+ const outFilePath = path.resolve(dir, `${id}_text.diff`);
98
+ await writeFile(outFilePath, `${textDiff.phrases.result}\n\n${textDiff.tokens.result}`, { encoding: 'utf8' });
99
+ text = {
100
+ matches: textDiff.tokens.matches,
101
+ diff: textDiff.tokens.changed ? textDiff.tokens.result : null,
67
102
  file: outFilePath,
68
103
  };
69
104
  }
70
- const htmlDiff = diffTree(a.url, b.url, screenshotA.domTree, screenshotB.domTree);
71
- const outFilePath = path.resolve(dir, `${id}_html.diff`);
72
- await writeFile(outFilePath, htmlDiff.result, { encoding: 'utf8' });
73
105
  screenshotResult[name] = {
74
106
  image,
75
- dom: {
76
- matches: htmlDiff.matches,
77
- diff: htmlDiff.changed ? htmlDiff.result : null,
78
- file: outFilePath,
79
- },
107
+ dom,
108
+ text,
80
109
  };
81
110
  }
82
111
  const result = {
@@ -1,7 +1,8 @@
1
1
  import type { AnalyzeOptions, URLPair } from './types.js';
2
+ import type { DealOptions } from '@d-zero/dealer';
2
3
  /**
3
4
  *
4
5
  * @param list
5
6
  * @param options
6
7
  */
7
- export declare function analyze(list: readonly URLPair[], options?: AnalyzeOptions): Promise<void>;
8
+ export declare function analyze(list: readonly URLPair[], options?: AnalyzeOptions & DealOptions): Promise<void>;
@@ -1,7 +1,8 @@
1
- import { mkdir } from 'node:fs/promises';
1
+ import { writeFile, mkdir } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { createProcess, deal } from '@d-zero/puppeteer-dealer';
4
4
  import c from 'ansi-colors';
5
+ import stripAnsi from 'strip-ansi';
5
6
  import { analyzeUrlList } from './modules/analize-url.js';
6
7
  import { score } from './utils.js';
7
8
  /**
@@ -22,25 +23,37 @@ export async function analyze(list, options) {
22
23
  list,
23
24
  dir,
24
25
  useOldMode,
25
- htmlDiffOnly: options?.htmlDiffOnly ?? false,
26
+ types: options?.types,
27
+ selector: options?.selector,
28
+ ignore: options?.ignore,
29
+ devices: options?.devices,
26
30
  hooks: options?.hooks ?? [],
27
31
  }, {
28
32
  ...options,
29
33
  headless: useOldMode ? 'shell' : true,
30
34
  });
31
- }, (result) => {
32
- results.push(result);
35
+ }, {
36
+ ...options,
37
+ each(result) {
38
+ results.push(result);
39
+ },
33
40
  });
34
41
  const output = [];
35
42
  for (const result of results) {
36
43
  output.push(c.gray(`${result.target.join(' vs ')}`));
37
- for (const [sizeName, { image, dom }] of Object.entries(result.screenshots)) {
44
+ for (const [sizeName, { image, dom, text }] of Object.entries(result.screenshots)) {
38
45
  if (image) {
39
46
  const { matches, file } = image;
40
47
  output.push(` ${c.bgMagenta(` ${sizeName} `)} ${score(matches, 0.9)} ${file}`);
41
48
  }
42
- output.push(` ${c.bgBlueBright(' HTML ')}: ${score(dom.matches, 0.995)} ${dom.file}`);
49
+ if (dom) {
50
+ output.push(` ${c.bgBlueBright(' HTML ')}: ${score(dom.matches, 0.995)} ${dom.file}`);
51
+ }
52
+ if (text) {
53
+ output.push(` ${c.bgGreenBright(' TEXT ')}: ${score(text.matches, 0.995)} ${text.file}`);
54
+ }
43
55
  }
44
56
  }
57
+ await writeFile(path.resolve(dir, 'RESULT.txt'), stripAnsi(output.join('\n').replaceAll(dir, '.')), 'utf8');
45
58
  process.stdout.write(output.join('\n') + '\n');
46
59
  }
package/dist/cli.js CHANGED
@@ -1,32 +1,52 @@
1
1
  #!/usr/bin/env node
2
- import minimist from 'minimist';
2
+ import { createCLI, parseCommonOptions, parseList } from '@d-zero/cli-core';
3
3
  import { analyze } from './analyze-main-process.js';
4
4
  import { freeze } from './freeze-main-process.js';
5
5
  import { readConfig } from './read-config.js';
6
- const cli = minimist(process.argv.slice(2), {
7
- alias: {
6
+ const { options, hasConfigFile } = createCLI({
7
+ aliases: {
8
8
  f: 'listfile',
9
+ t: 'type',
10
+ s: 'selector',
11
+ i: 'ignore',
12
+ },
13
+ usage: ['Usage: archaeologist -f <listfile> [--limit <number>]'],
14
+ parseArgs: (cli) => ({
15
+ ...parseCommonOptions(cli),
16
+ listfile: cli.listfile,
17
+ type: cli.type,
18
+ freeze: cli.freeze,
19
+ selector: cli.selector,
20
+ ignore: cli.ignore,
21
+ devices: cli.devices ??
22
+ // Alias for devices
23
+ cli.device,
24
+ }),
25
+ validateArgs: (options) => {
26
+ return !!(options.listfile?.length || options.freeze?.length);
9
27
  },
10
28
  });
11
- if (cli.listfile?.length) {
12
- const { pairList, hooks } = await readConfig(cli.listfile);
29
+ if (hasConfigFile) {
30
+ const { pairList, hooks } = await readConfig(options.listfile);
13
31
  await analyze(pairList, {
14
32
  hooks,
15
- limit: cli.limit ? Number.parseInt(cli.limit) : undefined,
16
- debug: !!cli.debug,
17
- htmlDiffOnly: !!cli.htmlDiffOnly,
33
+ types: options.type ? parseList(options.type) : undefined,
34
+ selector: options.selector,
35
+ ignore: options.ignore,
36
+ devices: options.devices ? parseList(options.devices) : undefined,
37
+ limit: options.limit,
38
+ debug: options.debug,
39
+ verbose: options.verbose,
18
40
  });
19
41
  process.exit(0);
20
42
  }
21
- if (cli.freeze) {
22
- const { pairList, hooks } = await readConfig(cli.freeze);
43
+ if (options.freeze) {
44
+ const { pairList, hooks } = await readConfig(options.freeze);
23
45
  const list = pairList.map(([urlA]) => urlA);
24
46
  await freeze(list, {
25
47
  hooks,
26
- limit: cli.limit ? Number.parseInt(cli.limit) : undefined,
27
- debug: !!cli.debug,
48
+ limit: options.limit,
49
+ debug: options.debug,
28
50
  });
29
51
  process.exit(0);
30
52
  }
31
- process.stderr.write('Usage: archaeologist -f <listfile> [--limit <number>]\n');
32
- process.exit(1);
@@ -0,0 +1,21 @@
1
+ /**
2
+ *
3
+ * @param urlA
4
+ * @param urlB
5
+ * @param phraseA
6
+ * @param phraseB
7
+ */
8
+ export declare function diffText(urlA: string, urlB: string, phraseA: string, phraseB: string): {
9
+ phrases: {
10
+ changed: boolean;
11
+ maxLine: number;
12
+ matches: number;
13
+ result: string;
14
+ };
15
+ tokens: {
16
+ changed: boolean;
17
+ maxLine: number;
18
+ matches: number;
19
+ result: string;
20
+ };
21
+ };
@@ -0,0 +1,45 @@
1
+ import { getTokenizer } from 'kuromojin';
2
+ import { diffTree } from './diff-tree.js';
3
+ const tokenizer = await getTokenizer();
4
+ /**
5
+ *
6
+ * @param text
7
+ */
8
+ function tokenList(text) {
9
+ return tokenizer
10
+ .tokenize(text)
11
+ .filter((token) => token.surface_form.trim() !== '')
12
+ .map((token) => `${token.surface_form}:${token.pos}:${token.pos_detail_1}`);
13
+ }
14
+ /**
15
+ *
16
+ * @param tokens
17
+ */
18
+ function frequencyMap(tokens) {
19
+ const map = new Map();
20
+ for (const token of tokens) {
21
+ map.set(token, (map.get(token) ?? 0) + 1);
22
+ }
23
+ return map
24
+ .entries()
25
+ .map(([token, frequency]) => `${token} x${frequency}`)
26
+ .toArray()
27
+ .toSorted((a, b) => a.localeCompare(b));
28
+ }
29
+ /**
30
+ *
31
+ * @param urlA
32
+ * @param urlB
33
+ * @param phraseA
34
+ * @param phraseB
35
+ */
36
+ export function diffText(urlA, urlB, phraseA, phraseB) {
37
+ const tokensA = tokenList(phraseA);
38
+ const tokensB = tokenList(phraseB);
39
+ const frequencyMapA = frequencyMap(tokensA).join('\n');
40
+ const frequencyMapB = frequencyMap(tokensB).join('\n');
41
+ return {
42
+ phrases: diffTree(urlA, urlB, phraseA, phraseB),
43
+ tokens: diffTree(urlA, urlB, frequencyMapA, frequencyMapB),
44
+ };
45
+ }
@@ -4,6 +4,9 @@ import type { Page } from 'puppeteer';
4
4
  export interface GetDataOptions {
5
5
  readonly hooks?: readonly PageHook[];
6
6
  readonly htmlDiffOnly?: boolean;
7
+ readonly selector?: string;
8
+ readonly ignore?: string;
9
+ readonly devices?: readonly string[];
7
10
  }
8
11
  /**
9
12
  *
@@ -1,4 +1,5 @@
1
1
  import { distill } from '@d-zero/html-distiller';
2
+ import { defaultSizes } from '@d-zero/puppeteer-page-scan';
2
3
  import { screenshotListener, screenshot } from '@d-zero/puppeteer-screenshot';
3
4
  /**
4
5
  *
@@ -9,19 +10,19 @@ import { screenshotListener, screenshot } from '@d-zero/puppeteer-screenshot';
9
10
  */
10
11
  export async function getData(page, url, options, update) {
11
12
  const htmlDiffOnly = options.htmlDiffOnly ?? false;
13
+ const devices = options.devices ?? ['desktop', 'mobile'];
14
+ const sizes = {};
15
+ for (const device of devices) {
16
+ // @ts-ignore
17
+ sizes[device] = defaultSizes[device];
18
+ }
12
19
  const screenshots = await screenshot(page, url, {
13
- sizes: {
14
- desktop: {
15
- width: 1280,
16
- },
17
- mobile: {
18
- width: 375,
19
- resolution: 2,
20
- },
21
- },
20
+ sizes,
22
21
  hooks: options?.hooks ?? [],
23
22
  listener: screenshotListener(update),
24
23
  domOnly: htmlDiffOnly,
24
+ selector: options.selector,
25
+ ignore: options.ignore,
25
26
  });
26
27
  const data = { url, screenshots: {} };
27
28
  for (const [sizeName, screenshot] of Object.entries(screenshots)) {
@@ -0,0 +1,5 @@
1
+ /**
2
+ *
3
+ * @param text
4
+ */
5
+ export declare function normalizeTextDocument(text: string): string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ *
3
+ * @param text
4
+ */
5
+ export function normalizeTextDocument(text) {
6
+ return (text
7
+ .trim()
8
+ // Spaces
9
+ .replaceAll(/\s+/g, '\n')
10
+ // Periods
11
+ .replaceAll('。', '。\n')
12
+ // Newlines
13
+ .replaceAll(/\n+/g, '\n')
14
+ .trim());
15
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ *
3
+ * @param typeQuery
4
+ */
5
+ export declare function parseTypes(typeQuery: string): string[];
@@ -0,0 +1,8 @@
1
+ /**
2
+ *
3
+ * @param typeQuery
4
+ */
5
+ export function parseTypes(typeQuery) {
6
+ const types = typeQuery.split(',').map((type) => type.trim());
7
+ return types;
8
+ }
@@ -1,17 +1,12 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
1
  import { readPageHooks } from '@d-zero/puppeteer-page-scan';
4
2
  import { toList } from '@d-zero/readtext/list';
5
- import fm from 'front-matter';
3
+ import { readConfigFile } from '@d-zero/shared/config-reader';
6
4
  /**
7
5
  *
8
6
  * @param filePath
9
7
  */
10
8
  export async function readConfig(filePath) {
11
- const fileContent = await fs.readFile(filePath, 'utf8');
12
- const content =
13
- // @ts-ignore
14
- fm(fileContent);
9
+ const { content, baseDir } = await readConfigFile(filePath);
15
10
  const urlList = toList(content.body);
16
11
  const pairList = urlList.map((urlStr) => {
17
12
  const url = new URL(urlStr);
@@ -20,7 +15,6 @@ export async function readConfig(filePath) {
20
15
  `${content.attributes.comparisonHost}${url.pathname}${url.search}`,
21
16
  ];
22
17
  });
23
- const baseDir = path.dirname(filePath);
24
18
  const hooks = await readPageHooks(content.attributes?.hooks ?? [], baseDir);
25
19
  return {
26
20
  pairList,
package/dist/types.d.ts CHANGED
@@ -13,7 +13,8 @@ export type Result = {
13
13
  };
14
14
  export type MediaResult = {
15
15
  image: ImageResult | null;
16
- dom: DOMResult;
16
+ dom: DOMResult | null;
17
+ text: TextResult | null;
17
18
  };
18
19
  export type ImageResult = {
19
20
  matches: number;
@@ -24,10 +25,18 @@ export type DOMResult = {
24
25
  diff: string | null;
25
26
  file: string;
26
27
  };
28
+ export type TextResult = {
29
+ matches: number;
30
+ diff: string | null;
31
+ file: string;
32
+ };
27
33
  export interface ArchaeologistOptions extends AnalyzeOptions {
28
34
  }
29
35
  export interface AnalyzeOptions extends GeneralOptions {
30
- readonly htmlDiffOnly?: boolean;
36
+ readonly types?: readonly string[];
37
+ readonly selector?: string;
38
+ readonly ignore?: string;
39
+ readonly devices?: readonly string[];
31
40
  }
32
41
  export interface FreezeOptions extends GeneralOptions {
33
42
  }
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@d-zero/archaeologist",
3
- "version": "2.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Uncover visual and HTML differences in web pages with precision",
5
5
  "author": "D-ZERO",
6
6
  "license": "MIT",
7
- "private": false,
8
7
  "publishConfig": {
9
8
  "access": "public"
10
9
  },
@@ -15,9 +14,7 @@
15
14
  "types": "./dist/index.d.ts"
16
15
  }
17
16
  },
18
- "bin": {
19
- "archaeologist": "./dist/cli.js"
20
- },
17
+ "bin": "./dist/cli.js",
21
18
  "files": [
22
19
  "dist"
23
20
  ],
@@ -27,27 +24,30 @@
27
24
  "clean": "tsc --build --clean"
28
25
  },
29
26
  "dependencies": {
30
- "@d-zero/fs": "0.2.0",
31
- "@d-zero/html-distiller": "1.0.3",
32
- "@d-zero/puppeteer-dealer": "0.4.0",
33
- "@d-zero/puppeteer-page-scan": "4.0.0",
34
- "@d-zero/puppeteer-screenshot": "3.0.1",
35
- "@d-zero/readtext": "1.1.3",
36
- "@d-zero/shared": "0.8.0",
27
+ "@d-zero/cli-core": "1.1.0",
28
+ "@d-zero/fs": "0.2.1",
29
+ "@d-zero/html-distiller": "1.0.4",
30
+ "@d-zero/puppeteer-dealer": "0.5.1",
31
+ "@d-zero/puppeteer-page-scan": "4.0.2",
32
+ "@d-zero/puppeteer-screenshot": "3.1.1",
33
+ "@d-zero/readtext": "1.1.5",
34
+ "@d-zero/shared": "0.9.1",
37
35
  "ansi-colors": "4.1.3",
38
- "diff": "8.0.1",
36
+ "diff": "8.0.2",
39
37
  "front-matter": "4.0.2",
40
38
  "jimp": "1.6.0",
39
+ "kuromojin": "3.0.1",
41
40
  "minimist": "1.2.8",
42
41
  "parse-diff": "0.11.1",
43
42
  "pixelmatch": "7.1.0",
44
43
  "pngjs": "7.0.0",
45
- "puppeteer": "24.9.0"
44
+ "puppeteer": "24.10.2",
45
+ "strip-ansi": "7.1.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/diff": "8.0.0",
49
49
  "@types/pixelmatch": "5.2.6",
50
50
  "@types/pngjs": "6.0.5"
51
51
  },
52
- "gitHead": "4e9cc7b87e0fef91b6f2d4edfb66ca9134b2491b"
52
+ "gitHead": "1218a023e62c79efeece6350d561f2e1906be7ea"
53
53
  }