(e2e) add threshold in regression test

When comparing PDF screenshots, we can have some
minor differences due to the different environments
(OS, fonts, etc.).
To avoid false positives in our regression
tests, we can set a threshold for the number of
different pixels allowed before considering the
test as failed.
If the test fails we will now report the PDF
and the differences to identify quickly
what are the regressions.
This commit is contained in:
Anthony LC
2026-02-06 09:34:47 +01:00
parent d02c6250c9
commit 1d8b730715
6 changed files with 403 additions and 181 deletions

View File

@@ -1,19 +1,18 @@
import fs from 'fs';
import path from 'path';
import { Page, expect, test } from '@playwright/test';
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import JSZip from 'jszip';
import { PDFParse } from 'pdf-parse';
import {
BrowserName,
TestLanguage,
createDoc,
verifyDocName,
waitForLanguageSwitch,
} from './utils-common';
import { openSuggestionMenu, writeInEditor } from './utils-editor';
import { comparePDFWithAssetFolder, overrideDocContent } from './utils-export';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -311,7 +310,14 @@ test.describe('Doc Export', () => {
test('it exports the doc to PDF with PRINT feature and checks regressions', async ({
page,
browserName,
}) => {
}, testInfo) => {
// PDF Binary comparison is different depending on the browser used
// We only run this test on Chromium to avoid having to maintain
// multiple sets of PDF fixtures
if (browserName !== 'chromium') {
test.skip();
}
await overrideDocContent({ page, browserName });
await page
@@ -343,11 +349,13 @@ test.describe('Doc Export', () => {
// );
// Assert the generated PDF matches the initial PDF regression fixture
await comparePDFWithAssetFolder(
pdfBuffer,
'doc-export-PDF-browser-regressions.pdf',
false,
);
await comparePDFWithAssetFolder({
originPdfBuffer: pdfBuffer,
filename: 'doc-export-PDF-browser-regressions.pdf',
compareTextContent: false,
comparePixel: false,
testInfo,
});
await expect(page.locator('#print-only-content-styles')).not.toBeAttached();
});
@@ -355,7 +363,7 @@ test.describe('Doc Export', () => {
test('it exports the doc to PDF and checks regressions', async ({
page,
browserName,
}) => {
}, testInfo) => {
// PDF Binary comparison is different depending on the browser used
// We only run this test on Chromium to avoid having to maintain
// multiple sets of PDF fixtures
@@ -380,161 +388,16 @@ test.describe('Doc Export', () => {
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
// If we need to update the PDF regression fixture, uncomment the line below
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
// If we need to update the PDF regression fixture, uncomment the line below
//await savePDFToAssetFolder(pdfBuffer, 'doc-export-regressions.pdf');
// Assert the generated PDF matches "assets/doc-export-regressions.pdf"
await comparePDFWithAssetFolder(pdfBuffer, 'doc-export-regressions.pdf');
await comparePDFWithAssetFolder({
originPdfBuffer: pdfBuffer,
filename: 'doc-export-regressions.pdf',
testInfo,
});
});
});
export const savePDFToAssetFolder = async (
pdfBuffer: Buffer,
filename: string,
) => {
const pdfPath = path.join(__dirname, 'assets', filename);
fs.writeFileSync(pdfPath, pdfBuffer);
};
export const comparePDFWithAssetFolder = async (
pdfBuffer: Buffer,
filename: string,
compareTextContent = true,
) => {
// Load reference PDF for comparison
const referencePdfPath = path.join(__dirname, 'assets', filename);
const referencePdfBuffer = fs.readFileSync(referencePdfPath);
// Parse both PDFs
const generatedPdf = new PDFParse({ data: pdfBuffer });
const referencePdf = new PDFParse({ data: referencePdfBuffer });
const [generatedInfo, referenceInfo] = await Promise.all([
generatedPdf.getInfo(),
referencePdf.getInfo(),
]);
const [generatedScreenshot, referenceScreenshot] = await Promise.all([
generatedPdf.getScreenshot(),
referencePdf.getScreenshot(),
]);
generatedScreenshot.pages[0].data;
const [generatedText, referenceText] = await Promise.all([
generatedPdf.getText(),
referencePdf.getText(),
]);
// Compare page count
expect(generatedInfo.total).toBe(referenceInfo.total);
/*
Compare text content
We make this optional because text extraction from PDFs can vary
slightly between environments and PDF versions, leading to false negatives.
Particularly with emojis which can be represented differently when
exporting or parsing the PDF.
*/
if (compareTextContent) {
expect(generatedText.text).toBe(referenceText.text);
}
// Compare screenshots page by page
for (let i = 0; i < generatedScreenshot.pages.length; i++) {
const genPage = generatedScreenshot.pages[i];
const refPage = referenceScreenshot.pages[i];
expect(genPage.width).toBe(refPage.width);
expect(genPage.height).toBe(refPage.height);
try {
expect(genPage.data).toStrictEqual(refPage.data);
} catch {
throw new Error(`PDF page ${i + 1} screenshot does not match reference.`);
}
}
};
/**
* Override the document content API response to use a test content
* This test content contains many blocks to facilitate testing
* @param page
*/
export const overrideDocContent = async ({
page,
browserName,
}: {
page: Page;
browserName: BrowserName;
}) => {
// Override content prop with assets/base-content-test-pdf.txt
await page.route(/\**\/documents\/\**/, async (route) => {
const request = route.request();
if (
request.method().includes('GET') &&
!request.url().includes('page=') &&
!request.url().includes('versions') &&
!request.url().includes('accesses') &&
!request.url().includes('invitations')
) {
const response = await route.fetch();
const json = await response.json();
json.content = fs.readFileSync(
path.join(__dirname, 'assets/base-content-test-pdf.txt'),
'utf-8',
);
void route.fulfill({
response,
body: JSON.stringify(json),
});
} else {
await route.continue();
}
});
const [randomDoc] = await createDoc(
page,
'doc-export-override-content',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.waitForTimeout(1000);
// Add Image SVG
await page.keyboard.press('Enter');
const { suggestionMenu } = await openSuggestionMenu({ page });
await suggestionMenu.getByText('Resizable image with caption').click();
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page
.locator('.--docs--editor-container img.bn-visual-media[src$=".svg"]')
.first();
await expect(image).toBeVisible();
await page.keyboard.press('Enter');
await page.waitForTimeout(1000);
// Add Image PNG
await openSuggestionMenu({ page });
await suggestionMenu.getByText('Resizable image with caption').click();
const fileChooserPNGPromise = page.waitForEvent('filechooser');
await page.getByText('Upload image').click();
const fileChooserPNG = await fileChooserPNGPromise;
await fileChooserPNG.setFiles(
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const imagePng = page
.locator('.--docs--editor-container img.bn-visual-media[src$=".png"]')
.first();
await expect(imagePng).toBeVisible();
await page.waitForTimeout(1000);
return randomDoc;
};

View File

@@ -8,6 +8,7 @@ test.beforeEach(async ({ page }) => {
test.describe('Home page', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('checks all the elements are visible', async ({ page }) => {
await page.goto('/docs/');

View File

@@ -1,4 +1,7 @@
import { Locator, Page, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
import { Locator, Page, TestInfo, expect } from '@playwright/test';
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
@@ -388,3 +391,30 @@ export const clickInGridMenu = async (
.click();
await page.getByRole('menuitem', { name: textButton }).click();
};
export const writeReport = async (
testInfo: TestInfo,
filename: string,
attachName: string,
buffer: Buffer,
contentType: string,
) => {
const REPORT_DIRNAME = 'extra-report';
const REPORT_NAME = 'test-results';
const outDir = testInfo
? path.join(testInfo.outputDir, REPORT_DIRNAME, path.parse(filename).name)
: path.join(
process.cwd(),
REPORT_NAME,
REPORT_DIRNAME,
path.parse(filename).name,
);
fs.mkdirSync(outDir, { recursive: true });
const pathToFile = path.join(outDir, filename);
fs.writeFileSync(pathToFile, buffer);
await testInfo.attach(attachName, {
path: pathToFile,
contentType: contentType,
});
};

View File

@@ -0,0 +1,239 @@
import fs from 'fs';
import path from 'path';
import { Page, TestInfo, expect } from '@playwright/test';
import { PDFParse } from 'pdf-parse';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';
import {
BrowserName,
createDoc,
verifyDocName,
writeReport,
} from './utils-common';
import { openSuggestionMenu } from './utils-editor';
/**
* Override the document content API response to use a test content
* This test content contains many blocks to facilitate testing
* @param page
*/
export const overrideDocContent = async ({
page,
browserName,
}: {
page: Page;
browserName: BrowserName;
}) => {
// Override content prop with assets/base-content-test-pdf.txt
await page.route(/\**\/documents\/\**/, async (route) => {
const request = route.request();
if (
request.method().includes('GET') &&
!request.url().includes('page=') &&
!request.url().includes('versions') &&
!request.url().includes('accesses') &&
!request.url().includes('invitations')
) {
const response = await route.fetch();
const json = await response.json();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
json.content = fs.readFileSync(
path.join(__dirname, 'assets/base-content-test-pdf.txt'),
'utf-8',
);
void route.fulfill({
response,
body: JSON.stringify(json),
});
} else {
await route.continue();
}
});
const [randomDoc] = await createDoc(
page,
'doc-export-override-content',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.waitForTimeout(1000);
// Add Image SVG
await page.keyboard.press('Enter');
const { suggestionMenu } = await openSuggestionMenu({ page });
await suggestionMenu.getByText('Resizable image with caption').click();
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page
.locator('.--docs--editor-container img.bn-visual-media[src$=".svg"]')
.first();
await expect(image).toBeVisible();
await page.keyboard.press('Enter');
await page.waitForTimeout(1000);
// Add Image PNG
await openSuggestionMenu({ page });
await suggestionMenu.getByText('Resizable image with caption').click();
const fileChooserPNGPromise = page.waitForEvent('filechooser');
await page.getByText('Upload image').click();
const fileChooserPNG = await fileChooserPNGPromise;
await fileChooserPNG.setFiles(
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const imagePng = page
.locator('.--docs--editor-container img.bn-visual-media[src$=".png"]')
.first();
await expect(imagePng).toBeVisible();
await page.waitForTimeout(1000);
return randomDoc;
};
export const savePDFToAssetFolder = async (
pdfBuffer: Buffer,
filename: string,
) => {
const pdfPath = path.join(__dirname, 'assets', filename);
fs.writeFileSync(pdfPath, pdfBuffer);
};
interface ComparePDFWithAssetFolderOptions {
originPdfBuffer: Buffer;
filename: string;
compareTextContent?: boolean;
comparePixel?: boolean;
testInfo?: TestInfo;
}
export const comparePDFWithAssetFolder = async ({
originPdfBuffer,
filename,
compareTextContent = true,
comparePixel = true,
testInfo,
}: ComparePDFWithAssetFolderOptions) => {
// Load reference PDF for comparison
const referencePdfPath = path.join(__dirname, 'assets', filename);
const referencePdfBuffer = fs.readFileSync(referencePdfPath);
// Parse both PDFs
const generatedPdf = new PDFParse({ data: originPdfBuffer });
const referencePdf = new PDFParse({ data: referencePdfBuffer });
const [generatedInfo, referenceInfo] = await Promise.all([
generatedPdf.getInfo(),
referencePdf.getInfo(),
]);
const [generatedScreenshot, referenceScreenshot] = await Promise.all([
generatedPdf.getScreenshot(),
referencePdf.getScreenshot(),
]);
const [generatedText, referenceText] = await Promise.all([
generatedPdf.getText(),
referencePdf.getText(),
]);
// Compare page count
expect(generatedInfo.total).toBe(referenceInfo.total);
/*
Compare text content
We make this optional because text extraction from PDFs can vary
slightly between environments and PDF versions, leading to false negatives.
Particularly with emojis which can be represented differently when
exporting or parsing the PDF.
*/
if (compareTextContent) {
expect(generatedText.text).toBe(referenceText.text);
}
// Compare screenshots page by page
for (let i = 0; i < generatedScreenshot.pages.length; i++) {
const genPage = generatedScreenshot.pages[i];
const refPage = referenceScreenshot.pages[i];
const genPng = PNG.sync.read(Buffer.from(genPage.data));
const refPng = PNG.sync.read(Buffer.from(refPage.data));
// Compare actual raster dimensions (integers)
expect(genPng.width).toBe(refPng.width);
expect(genPng.height).toBe(refPng.height);
if (!comparePixel) {
continue;
}
const diffPng = new PNG({ width: genPng.width, height: genPng.height });
const numDiffPixels = pixelmatch(
genPng.data,
refPng.data,
diffPng.data,
genPng.width,
genPng.height,
{ threshold: 0.1, includeAA: false },
);
const totalPixels = genPng.width * genPng.height;
const diffRatio = numDiffPixels / totalPixels;
const maxDiffRatio = 0.0005;
try {
expect(numDiffPixels).toBeLessThan(0.0005);
} catch {
if (testInfo) {
const pageNo = String(i + 1).padStart(2, '0');
await writeReport(
testInfo,
`generated.pdf`,
`pdf-generated`,
originPdfBuffer,
'application/pdf',
);
await writeReport(
testInfo,
`reference.pdf`,
`pdf-reference`,
referencePdfBuffer,
'application/pdf',
);
await writeReport(
testInfo,
`page-${pageNo}-diff.png`,
`page-${pageNo}-diff`,
PNG.sync.write(diffPng),
'image/png',
);
await writeReport(
testInfo,
`page-${pageNo}-generated.png`,
`page-${pageNo}-generated`,
PNG.sync.write(genPng),
'image/png',
);
await writeReport(
testInfo,
`page-${pageNo}-reference.png`,
`page-${pageNo}-reference`,
PNG.sync.write(refPng),
'image/png',
);
}
throw new Error(
`PDF visual regression: ${filename} page ${i + 1} diffRatio=${diffRatio.toFixed(6)} (${numDiffPixels} px) > ${maxDiffRatio}`,
);
}
}
};

View File

@@ -22,8 +22,11 @@
"typescript": "*"
},
"dependencies": {
"@types/pngjs": "6.0.5",
"convert-stream": "1.0.2",
"pdf-parse": "2.4.5"
"pdf-parse": "2.4.5",
"pixelmatch": "7.1.0",
"pngjs": "7.0.0"
},
"packageManager": "yarn@1.22.22"
}

View File

@@ -1802,30 +1802,25 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b"
integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==
"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0":
"@eslint-community/eslint-utils@^4.7.0":
version "4.9.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3"
integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/eslint-utils@^4.9.1":
"@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1":
version "4.9.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.2":
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2":
version "4.12.2"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
"@eslint-community/regexpp@^4.12.1":
version "4.12.1"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
"@eslint/config-array@^0.21.1":
version "0.21.1"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713"
@@ -1850,9 +1845,9 @@
"@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964"
integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==
version "3.3.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz#26393a0806501b5e2b6a43aa588a4d8df67880ac"
integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@@ -1860,7 +1855,7 @@
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
js-yaml "^4.1.1"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
@@ -2654,52 +2649,107 @@
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz#2779ca5c8aaeb46c85eb72d29f1eb34efd46fb45"
integrity sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==
"@napi-rs/canvas-android-arm64@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.90.tgz#c5f2a17e68395f8c695a90bff4356dbdce4bc5d3"
integrity sha512-3JBULVF+BIgr7yy7Rf8UjfbkfFx4CtXrkJFD1MDgKJ83b56o0U9ciT8ZGTCNmwWkzu8RbNKlyqPP3KYRG88y7Q==
"@napi-rs/canvas-darwin-arm64@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz#638eaa2d0a2a373c7d15748743182718dcd95c4b"
integrity sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==
"@napi-rs/canvas-darwin-arm64@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.90.tgz#6141f9dcaffebaa7c5ca3cbdcc1664298bd817d3"
integrity sha512-L8XVTXl+8vd8u7nPqcX77NyG5RuFdVsJapQrKV9WE3jBayq1aSMht/IH7Dwiz/RNJ86E5ZSg9pyUPFIlx52PZA==
"@napi-rs/canvas-darwin-x64@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz#bd6bc048dbd4b02b9620d9d07117ed93e6970978"
integrity sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==
"@napi-rs/canvas-darwin-x64@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.90.tgz#4f35e54b33ccd437d577e91075d4af1a00f042a9"
integrity sha512-h0ukhlnGhacbn798VWYTQZpf6JPDzQYaow+vtQ2Fat7j7ImDdpg6tfeqvOTO1r8wS+s+VhBIFITC7aA1Aik0ZQ==
"@napi-rs/canvas-linux-arm-gnueabihf@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz#ce6bfbeb19d9234c42df5c384e5989aa7d734789"
integrity sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==
"@napi-rs/canvas-linux-arm-gnueabihf@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.90.tgz#1ab8a389c853f4a1c48d37ad11aca3ccbb620165"
integrity sha512-JCvTl99b/RfdBtgftqrf+5UNF7GIbp7c5YBFZ+Bd6++4Y3phaXG/4vD9ZcF1bw1P4VpALagHmxvodHuQ9/TfTg==
"@napi-rs/canvas-linux-arm64-gnu@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz#3b7a7832fef763826fa5fb740d5757204e52607d"
integrity sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==
"@napi-rs/canvas-linux-arm64-gnu@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.90.tgz#f4cb9d795e7b6b4cda6846a3e90da96dfb9f2bb3"
integrity sha512-vbWFp8lrP8NIM5L4zNOwnsqKIkJo0+GIRUDcLFV9XEJCptCc1FY6/tM02PT7GN4PBgochUPB1nBHdji6q3ieyQ==
"@napi-rs/canvas-linux-arm64-musl@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz#d8ccd91f31d70760628623cd575134ada17690a3"
integrity sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==
"@napi-rs/canvas-linux-arm64-musl@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.90.tgz#233160e64397370d84068c258cf3ee927f6d8730"
integrity sha512-8Bc0BgGEeOaux4EfIfNzcRRw0JE+lO9v6RWQFCJNM9dJFE4QJffTf88hnmbOaI6TEMpgWOKipbha3dpIdUqb/g==
"@napi-rs/canvas-linux-riscv64-gnu@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz#927a3b859a0e3c691beaf52a19bc4736c4ffc9b8"
integrity sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==
"@napi-rs/canvas-linux-riscv64-gnu@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.90.tgz#9d8dbecc8653cd7611ab0fd241c3909fee18a423"
integrity sha512-0iiVDG5IH+gJb/YUrY/pRdbsjcgvwUmeckL/0gShWAA7004ygX2ST69M1wcfyxXrzFYjdF8S/Sn6aCAeBi89XQ==
"@napi-rs/canvas-linux-x64-gnu@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz#25c0416bcedd6fadc15295e9afa8d9697232050c"
integrity sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==
"@napi-rs/canvas-linux-x64-gnu@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.90.tgz#a21fb6fce85f9e85f4f52eb7db93b4f79c2d66d7"
integrity sha512-SkKmlHMvA5spXuKfh7p6TsScDf7lp5XlMbiUhjdCtWdOS6Qke/A4qGVOciy6piIUCJibL+YX+IgdGqzm2Mpx/w==
"@napi-rs/canvas-linux-x64-musl@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz#de85d644e09120a60996bbe165cc2efee804551b"
integrity sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==
"@napi-rs/canvas-linux-x64-musl@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.90.tgz#d1276fa2fc857a4133b6dc70257743b2b1210c96"
integrity sha512-o6QgS10gAS4vvELGDOOWYfmERXtkVRYFWBCjomILWfMgCvBVutn8M97fsMW5CrEuJI8YuxuJ7U+/DQ9oG93vDA==
"@napi-rs/canvas-win32-arm64-msvc@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.90.tgz#293325cbdad36a40654760699b87c59b9003cae7"
integrity sha512-2UHO/DC1oyuSjeCAhHA0bTD9qsg58kknRqjJqRfvIEFtdqdtNTcWXMCT9rQCuJ8Yx5ldhyh2SSp7+UDqD2tXZQ==
"@napi-rs/canvas-win32-x64-msvc@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz#6bb95885d9377912d71f1372fc1916fb54d6ef0a"
integrity sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==
"@napi-rs/canvas@0.1.80", "@napi-rs/canvas@^0.1.80":
"@napi-rs/canvas-win32-x64-msvc@0.1.90":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.90.tgz#57601aa9be595693168d27fb0cd0c4060284c668"
integrity sha512-48CxEbzua5BP4+OumSZdi3+9fNiRO8cGNBlO2bKwx1PoyD1R2AXzPtqd/no1f1uSl0W2+ihOO1v3pqT3USbmgQ==
"@napi-rs/canvas@0.1.80":
version "0.1.80"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas/-/canvas-0.1.80.tgz#53615bea56fd94e07331ab13caa7a39efc4914ab"
integrity sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==
@@ -2715,6 +2765,23 @@
"@napi-rs/canvas-linux-x64-musl" "0.1.80"
"@napi-rs/canvas-win32-x64-msvc" "0.1.80"
"@napi-rs/canvas@^0.1.80":
version "0.1.90"
resolved "https://registry.yarnpkg.com/@napi-rs/canvas/-/canvas-0.1.90.tgz#f82e8f52dacc552e7feb9a136d77d9002374bad7"
integrity sha512-vO9j7TfwF9qYCoTOPO39yPLreTRslBVOaeIwhDZkizDvBb0MounnTl0yeWUMBxP4Pnkg9Sv+3eQwpxNUmTwt0w==
optionalDependencies:
"@napi-rs/canvas-android-arm64" "0.1.90"
"@napi-rs/canvas-darwin-arm64" "0.1.90"
"@napi-rs/canvas-darwin-x64" "0.1.90"
"@napi-rs/canvas-linux-arm-gnueabihf" "0.1.90"
"@napi-rs/canvas-linux-arm64-gnu" "0.1.90"
"@napi-rs/canvas-linux-arm64-musl" "0.1.90"
"@napi-rs/canvas-linux-riscv64-gnu" "0.1.90"
"@napi-rs/canvas-linux-x64-gnu" "0.1.90"
"@napi-rs/canvas-linux-x64-musl" "0.1.90"
"@napi-rs/canvas-win32-arm64-msvc" "0.1.90"
"@napi-rs/canvas-win32-x64-msvc" "0.1.90"
"@napi-rs/wasm-runtime@^0.2.11":
version "0.2.12"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2"
@@ -6987,6 +7054,13 @@
pg-protocol "*"
pg-types "^2.2.0"
"@types/pngjs@6.0.5":
version "6.0.5"
resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.5.tgz#6dec2f7eb8284543ca4e423f3c09b119fa939ea3"
integrity sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==
dependencies:
"@types/node" "*"
"@types/qs@*":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5"
@@ -9913,9 +9987,9 @@ esprima@^4.0.0:
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
version "1.7.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d"
integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==
dependencies:
estraverse "^5.1.0"
@@ -12007,7 +12081,7 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.1.0:
js-yaml@^4.1.0, js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
@@ -13533,6 +13607,13 @@ pirates@^4.0.7:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22"
integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==
pixelmatch@7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-7.1.0.tgz#9d59bddc8c779340e791106c0f245ac33ae4d113"
integrity sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==
dependencies:
pngjs "^7.0.0"
pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -13561,6 +13642,11 @@ plimit-lit@^1.2.6:
dependencies:
queue-lit "^1.5.1"
pngjs@7.0.0, pngjs@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26"
integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==
possible-typed-array-names@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"