diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts index 6cf92154..1cdccea0 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts @@ -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; -}; diff --git a/src/frontend/apps/e2e/__tests__/app-impress/home.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/home.spec.ts index e5d72bd8..7799ba96 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/home.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/home.spec.ts @@ -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/'); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index 7e7db0b8..3f82ce3c 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -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, + }); +}; diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts new file mode 100644 index 00000000..febec69c --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts @@ -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}`, + ); + } + } +}; diff --git a/src/frontend/apps/e2e/package.json b/src/frontend/apps/e2e/package.json index 7dc44eed..96fb4dc8 100644 --- a/src/frontend/apps/e2e/package.json +++ b/src/frontend/apps/e2e/package.json @@ -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" } diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index e4ad314b..1b134fbd 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -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"