Skip to content

Commit d38ff06

Browse files
authored
Merge pull request #6857 from Countly/QT-344
[QT-344] Verify downloaded report pdf
2 parents 98bb56c + ca7ec47 commit d38ff06

File tree

6 files changed

+1397
-20
lines changed

6 files changed

+1397
-20
lines changed

.github/workflows/main.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,13 @@ jobs:
278278
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -O /tmp/chrome.deb
279279
apt install -y /tmp/chrome.deb
280280
281+
- name: Install Sharp dependencies for image processing
282+
shell: bash
283+
run: |
284+
export DEBIAN_FRONTEND=noninteractive
285+
apt-get update -y
286+
apt-get install -y libvips-dev
287+
281288
- name: Copy code
282289
shell: bash
283290
run: cp -rf ./* /opt/countly
@@ -331,6 +338,6 @@ jobs:
331338
working-directory: /opt/countly/ui-tests/cypress
332339
run: |
333340
ARTIFACT_ARCHIVE_NAME="$(date '+%Y%m%d-%H.%M')_${GITHUB_REPOSITORY#*/}_CI#${{ github.run_number }}_${{ matrix.test_type }}.tar.gz"
334-
mkdir -p screenshots videos
335-
tar zcvf "$ARTIFACT_ARCHIVE_NAME" screenshots videos
341+
mkdir -p screenshots videos downloads
342+
tar zcvf "$ARTIFACT_ARCHIVE_NAME" screenshots videos downloads
336343
curl -o /tmp/uploader.log -u "${{ secrets.BOX_UPLOAD_AUTH }}" ${{ secrets.BOX_UPLOAD_PATH }} -T "$ARTIFACT_ARCHIVE_NAME"

ui-tests/cypress.config.sample.js

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
const { defineConfig } = require("cypress");
2-
const fs = require('fs');
2+
const fs = require("fs");
3+
const pdfjsLib = require("pdfjs-dist/legacy/build/pdf.js");
4+
const { PNG } = require("pngjs");
5+
const sharp = require("sharp");
6+
7+
// Define missing DOMMatrix in Node context (for pdfjs)
8+
if (typeof global.DOMMatrix === "undefined") {
9+
global.DOMMatrix = class DOMMatrix { };
10+
}
311

412
module.exports = defineConfig({
513
e2e: {
@@ -14,20 +22,116 @@ module.exports = defineConfig({
1422
watchForFileChanges: true,
1523
video: true,
1624
setupNodeEvents(on, config) {
17-
on('after:spec', (spec, results) => {
18-
if (results && results.video) {
19-
const failures = results.tests.some((test) =>
20-
test.attempts.some((attempt) => attempt.state === 'failed')
21-
);
22-
if (!failures) {
23-
// delete the video if the spec passed and no tests retried
24-
const videoPath = results.video;
25-
if (fs.existsSync(videoPath)) {
26-
fs.unlinkSync(videoPath);
25+
// Task: verify PDF images, logo, and text content
26+
on("task", {
27+
async verifyPdf({ filePath, options = {} }) {
28+
// options: { referenceLogoPath: string }
29+
30+
// Load PDF file
31+
const data = new Uint8Array(fs.readFileSync(filePath));
32+
const pdfDoc = await pdfjsLib.getDocument({ data }).promise;
33+
34+
// Import pixelmatch only if logo check is needed
35+
let pixelmatch;
36+
const doLogoCheck = !!options.referenceLogoPath;
37+
if (doLogoCheck) {
38+
const pm = await import("pixelmatch");
39+
pixelmatch = pm.default;
40+
}
41+
42+
let hasImage = false;
43+
let logoFound = false;
44+
let extractedText = ""; //store text here
45+
46+
// Loop through all pages
47+
for (let p = 1; p <= pdfDoc.numPages; p++) {
48+
const page = await pdfDoc.getPage(p);
49+
50+
//Extract text content from page
51+
const textContent = await page.getTextContent();
52+
const pageText = textContent.items.map((item) => item.str).join(" ");
53+
extractedText += pageText + "\n";
54+
55+
//Check for image operators
56+
const ops = await page.getOperatorList();
57+
58+
for (let i = 0; i < ops.fnArray.length; i++) {
59+
const fn = ops.fnArray[i];
60+
const args = ops.argsArray[i];
61+
62+
if (
63+
fn === pdfjsLib.OPS.paintImageXObject ||
64+
fn === pdfjsLib.OPS.paintJpegXObject ||
65+
fn === pdfjsLib.OPS.paintInlineImageXObject
66+
) {
67+
hasImage = true;
68+
69+
if (doLogoCheck && args[0]) {
70+
const objName = args[0];
71+
const imgData = await page.objs.get(objName);
72+
if (!imgData) {
73+
continue;
74+
}
75+
76+
const pdfImg = new PNG({ width: imgData.width, height: imgData.height });
77+
pdfImg.data = imgData.data;
78+
79+
const pdfBuffer = PNG.sync.write(pdfImg);
80+
const refLogo = PNG.sync.read(fs.readFileSync(options.referenceLogoPath));
81+
82+
const resizedPdfBuffer = await sharp(pdfBuffer)
83+
.resize(refLogo.width, refLogo.height)
84+
.png()
85+
.toBuffer();
86+
87+
const resizedPdfImg = PNG.sync.read(resizedPdfBuffer);
88+
89+
const diff = new PNG({ width: refLogo.width, height: refLogo.height });
90+
const mismatched = pixelmatch(
91+
refLogo.data,
92+
resizedPdfImg.data,
93+
diff.data,
94+
refLogo.width,
95+
refLogo.height,
96+
{ threshold: 0.1 }
97+
);
98+
99+
if (mismatched === 0) {
100+
logoFound = true;
101+
break;
102+
}
103+
}
104+
}
27105
}
106+
107+
if ((doLogoCheck && logoFound) || (!doLogoCheck && hasImage)) {
108+
break;
109+
}
110+
}
111+
112+
if (doLogoCheck && !logoFound) {
113+
throw new Error("Logo in PDF does not match reference image");
114+
}
115+
116+
//Return with extracted text
117+
return {
118+
hasImage,
119+
logoFound,
120+
text: extractedText,
121+
numPages: pdfDoc.numPages
122+
};
123+
},
124+
});
125+
126+
on("after:spec", (spec, results) => {
127+
if (results?.video) {
128+
const hasFailures = results.tests.some((t) => t.attempts.some((a) => a.state === "failed"));
129+
if (!hasFailures && fs.existsSync(results.video)) {
130+
fs.unlinkSync(results.video);
28131
}
29132
}
30133
});
134+
31135
on("before:browser:launch", (browser, launchOptions) => {
32136
if (["chrome", "edge", "electron"].includes(browser.name)) {
33137
if (browser.isHeadless) {
@@ -42,6 +146,4 @@ module.exports = defineConfig({
42146
});
43147
},
44148
},
45-
});
46-
47-
149+
});

ui-tests/cypress/e2e/dashboard/dashboards/dashboards.cy.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
/* eslint-env mocha */
2+
/* global cy, Cypress, expect */
3+
14
import user from '../../../fixtures/user.json';
5+
const getApiKey = require('../../../api/getApiKey');
26
const navigationHelpers = require('../../../support/navigations');
37
const helper = require('../../../support/helper');
48
const loginHelpers = require('../../../lib/login/login');
@@ -17,7 +21,7 @@ describe('Create New Custom Dashboard', () => {
1721
});
1822

1923
it(`
20-
Create a custom dashboard with a widget and an email report, and then verify the report preview using these parameters:
24+
Create a custom dashboard with a widget and an email report, verify the report preview, and validate the downloaded PDF content:
2125
//***Dashboard***
2226
Dashboard Visibility: All Users (default)
2327
//***Widget***
@@ -84,6 +88,53 @@ describe('Create New Custom Dashboard', () => {
8488

8589
reportHelper.openReportPreviewButton();
8690
reportHelper.verifyReportPreviewPageImage();
91+
92+
// Get the current URL
93+
cy.url().then((currentUrl) => {
94+
95+
//Get API key
96+
getApiKey.request(user.username, user.password).then((apiKey) => {
97+
98+
//Change preview to PDF and add api_key parameter
99+
const urlObj = new URL(currentUrl);
100+
urlObj.pathname = urlObj.pathname.replace('preview', 'pdf');
101+
urlObj.searchParams.set('api_key', apiKey);
102+
const pdfURL = urlObj.toString();
103+
104+
cy.log('Generated PDF URL:', pdfURL);
105+
106+
//Download the PDF and verify its content
107+
cy.request({
108+
url: pdfURL,
109+
encoding: 'binary',
110+
timeout: 120000,
111+
}).then((response) => {
112+
expect(response.status).to.eq(200);
113+
expect(response.headers['content-type']).to.include('application/pdf');
114+
115+
const buf = Buffer.from(response.body, 'binary');
116+
expect(buf.slice(0, 4).toString()).to.eq('%PDF');
117+
expect(buf.length).to.be.greaterThan(50000); // More than 50KB to ensure it's not empty
118+
119+
// Save the PDF to disk
120+
cy.writeFile('cypress/downloads/generated-report.pdf', buf);
121+
});
122+
});
123+
});
124+
125+
// Verify PDF content
126+
cy.task("verifyPdf", {
127+
filePath: "cypress/downloads/generated-report.pdf",
128+
options: {
129+
referenceLogoPath: "cypress/fixtures/testFiles/countly-logo.png",
130+
checkText: true
131+
}
132+
}).then((result) => {
133+
expect(result.logoFound).to.be.true;
134+
expect(result.hasImage).to.be.true;
135+
expect(result.text).to.include("Sent by Countly | Unsubscribe");
136+
expect(result.text).to.include("Report settings | Get help");
137+
});
87138
});
88139

89140
it(`Create a private custom dashboard and duplicate it and edit it and delete it then verify the flow`, function() {
2.67 KB
Loading

0 commit comments

Comments
 (0)