diff --git a/.gitignore b/.gitignore index e79a711369d..19a6ea80be9 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ webapp/analyzer.report.html user-password-change.txt user-password-change.csv /tests/helm/values.yaml +/tests/e2e/visual/images/*.png + diff --git a/package-lock.json b/package-lock.json index 35edabae314..5534437eda6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,6 +154,7 @@ "rewire": "^7.0.0", "rosie": "^2.1.0", "sass": "^1.67.0", + "sharp": "^0.33.5", "shellcheck": "^2.2.0", "sinon": "^16.1.0", "tail": "^2.2.6", @@ -7645,7 +7646,7 @@ "source-map-support": "0.5.21", "teen_process": "2.2.0", "type-fest": "4.23.0", - "typescript": "5.3.3", + "typescript": "5.5.4", "yaml": "2.5.0", "yargs": "17.7.2", "yargs-parser": "21.1.1" @@ -33132,6 +33133,446 @@ "sha.js": "bin.js" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/sharp/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/sharp/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/sharp/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/shasum-object": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", diff --git a/package.json b/package.json index 7d026545d27..9fc5cd963e8 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "unit-haproxy-healthcheck": "cd haproxy-healthcheck && make test", "unit-cht-deploy": "cd scripts/deploy && npm test", "wdio-default-mobile-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && wdio run ./tests/e2e/default-mobile/wdio.conf.js --suite=all", + "wdio-visual-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && wdio run ./tests/e2e/visual/wdio.conf.js --suite=all", "wdio-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && wdio run ./tests/e2e/default/wdio.conf.js", "apdex-test": "wdio run ./tests/performance/apdex-score/wdio.conf.js", "-- CI SCRIPTS ": "-----------------------------------------------------------------------------------------------", @@ -156,6 +157,7 @@ "rewire": "^7.0.0", "rosie": "^2.1.0", "sass": "^1.67.0", + "sharp": "^0.33.5", "shellcheck": "^2.2.0", "sinon": "^16.1.0", "tail": "^2.2.6", diff --git a/tests/e2e/visual/contacts/list-view-login-visual.wdio-spec.js b/tests/e2e/visual/contacts/list-view-login-visual.wdio-spec.js new file mode 100644 index 00000000000..a6925ca6fd4 --- /dev/null +++ b/tests/e2e/visual/contacts/list-view-login-visual.wdio-spec.js @@ -0,0 +1,133 @@ +const commonPage = require('@page-objects/default/common/common.wdio.page'); +const contactPage = require('@page-objects/default/contacts/contacts.wdio.page'); +const loginPage = require('@page-objects/default/login/login.wdio.page'); +const dataFactory = require('@factories/cht/generate'); +const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); +const utils = require('@utils'); + +const { resizeWindowForScreenshots, generateScreenshot } = require('@utils/screenshots'); + +describe('Contact List Page', () => { + const updateRolePermissions = async (roleValue, addPermissions, removePermissions = []) => { + const roles = [roleValue]; + const settings = await utils.getSettings(); + const permissions = await utils.getUpdatedPermissions(roles, addPermissions, removePermissions); + await utils.updateSettings( + { roles: settings.roles, permissions }, + { revert: true, ignoreReload: true, refresh: true, sync: true } + ); + }; + + const docs = dataFactory.createHierarchy({ + name: 'Janet Mwangi', + user: true, + nbrClinics: 10, + nbrPersons: 4, + useRealNames: true, + }); + + before(async () => { + await resizeWindowForScreenshots(); + await utils.saveDocs([...docs.places, ...docs.clinics, ...docs.persons, ...docs.reports]); + await utils.createUsers([docs.user]); + }); + + after(async () => { + await utils.deleteUsers([docs.user]); + await utils.revertDb([/^form:/], true); + }); + + beforeEach(async () => { + await loginPage.login(docs.user); + }); + + afterEach(async () => { + await commonPage.logout(); + }); + + describe('Log in', () => { + it('should show contacts page tab '+ + 'when can_view_contact and can_view_contacts_tab permissions are enabled', async () => { + await (await commonPage.contactsTab()).waitForDisplayed(); + await generateScreenshot('contact-page', 'tab-visible'); + await commonPage.openHamburgerMenu(); + await generateScreenshot('contact-page', 'menu-opened'); + await commonPage.closeHamburgerMenu(); + await commonPage.goToPeople(); + expect(await commonPage.isPeopleListPresent()).to.be.true; + await generateScreenshot('contact-page', 'people-list-visible'); + await commonPage.goToReports(); + await reportsPage.openFirstReport(); + await reportsPage.rightPanelSelectors.patientName().waitForClickable(); + await generateScreenshot('contact-page', 'reports-visible'); + await reportsPage.rightPanelSelectors.patientName().click(); + await contactPage.waitForContactLoaded(); + await generateScreenshot('contact-page', 'contact-loaded'); + await commonPage.goToMessages(); + }); + + it('should hide contacts page as tab and from menu option ' + + 'when can_view_contacts_tab permissions is enable but can_view_contact permission is not', async () => { + await updateRolePermissions('chw', [], ['can_view_contacts']); + await commonPage.waitForPageLoaded(); + await commonPage.goToMessages(); + await (await commonPage.contactsTab()).waitForDisplayed({ reverse: true }); + await generateScreenshot('contact-page', 'no-tab-visible'); + await commonPage.openHamburgerMenu(); + await (await commonPage.contactsButton()).waitForClickable({ reverse: true }); + await generateScreenshot('contact-page', 'no-menu-option'); + await commonPage.closeHamburgerMenu(); + await commonPage.goToReports(); + await reportsPage.openFirstReport(); + await (reportsPage.rightPanelSelectors.patientName()).waitForClickable(); + await generateScreenshot('contact-page', 'report-view-no-contacts'); + await reportsPage.rightPanelSelectors.patientName().click(); + await generateScreenshot('contact-page', 'report-no-contact-loaded'); + await commonPage.goToMessages(); + }); + + it('should hide contacts page as tab, show from menu option ' + + 'when can_view_contact permissions is enable but can_view_contact permission is not', async () => { + await updateRolePermissions('chw', ['can_view_contacts'], ['can_view_contacts_tab']); + await commonPage.waitForPageLoaded(); + await commonPage.goToMessages(); + await (await commonPage.contactsTab()).waitForDisplayed({ reverse: true }); + await generateScreenshot('contact-page', 'no-tab-visible'); + await commonPage.openHamburgerMenu(); + await (await commonPage.contactsButton()).waitForClickable(); + await generateScreenshot('contact-page', 'menu-option-visible'); + await (await commonPage.contactsButton()).click(); + expect(await commonPage.isPeopleListPresent()).to.be.true; + await commonPage.waitForPageLoaded(); + await generateScreenshot('contact-page', 'contacts-in-people-list'); + await commonPage.goToReports(); + await reportsPage.openFirstReport(); + await (reportsPage.rightPanelSelectors.patientName()).waitForClickable(); + await generateScreenshot('contact-page', 'report-view-with-contacts'); + await reportsPage.rightPanelSelectors.patientName().click(); + await contactPage.waitForContactLoaded(); + await generateScreenshot('contact-page', 'report-contact-loaded'); + await commonPage.goToMessages(); + }); + + it('should hide contacts page as a tab and from menu option ' + + 'when can_view_contact and can_view_contact permissions are disable', async () => { + await updateRolePermissions('chw', [], ['can_view_contacts_tab', 'can_view_contacts']); + await commonPage.waitForPageLoaded(); + await commonPage.goToMessages(); + await (await commonPage.contactsTab()).waitForDisplayed({ reverse: true }); + await generateScreenshot('contact-page', 'no-tab-visible-oPerms'); + await commonPage.openHamburgerMenu(); + await (await commonPage.contactsButton()).waitForClickable({ reverse: true }); + await generateScreenshot('contact-page', 'no-menu-option-no-Perms'); + await commonPage.closeHamburgerMenu(); + await commonPage.goToReports(); + await reportsPage.openFirstReport(); + await (reportsPage.rightPanelSelectors.patientName()).waitForClickable(); + await generateScreenshot('contact-page', 'report-view-no-contacts-no-perms'); + await reportsPage.rightPanelSelectors.patientName().click(); + await generateScreenshot('contact-page', 'report-no-contact-loaded-no-perms'); + await commonPage.goToMessages(); + }); + }); +}); diff --git a/tests/e2e/visual/wdio.conf.js b/tests/e2e/visual/wdio.conf.js new file mode 100644 index 00000000000..1ba9009f00a --- /dev/null +++ b/tests/e2e/visual/wdio.conf.js @@ -0,0 +1,36 @@ +const wdioBaseConfig = require('../../wdio.conf'); + +const chai = require('chai'); +chai.use(require('chai-exclude')); + +const mobileCapability = { + ...wdioBaseConfig.config.capabilities[0], + 'goog:chromeOptions': { + ...wdioBaseConfig.config.capabilities[0]['goog:chromeOptions'], + args: [ + ...wdioBaseConfig.config.capabilities[0]['goog:chromeOptions'].args, + 'window-size=450,700', + ], + }, +}; + +const desktopCapability = { + ...wdioBaseConfig.config.capabilities[0], + 'goog:chromeOptions': { + ...wdioBaseConfig.config.capabilities[0]['goog:chromeOptions'], + args: [ + ...wdioBaseConfig.config.capabilities[0]['goog:chromeOptions'].args, + 'window-size=1000,800', + ], + }, +}; + +exports.config = Object.assign(wdioBaseConfig.config, { + suites: { + all: [ + './**/*.wdio-spec.js', + ] + }, + capabilities: [mobileCapability, desktopCapability], + maxInstances: 1, +}); diff --git a/tests/factories/cht/generate.js b/tests/factories/cht/generate.js index 5fb43e46c9d..86ebbac7d13 100644 --- a/tests/factories/cht/generate.js +++ b/tests/factories/cht/generate.js @@ -4,14 +4,25 @@ const personFactory = require('@factories/cht/contacts/person'); const deliveryFactory = require('@factories/cht/reports/delivery'); const pregnancyFactory = require('@factories/cht/reports/pregnancy'); const pregnancyVisitFactory = require('@factories/cht/reports/pregnancy-visit'); + +// Fixed collection of real-world data +const FIRST_NAMES = ['Amanda', 'Beatrice', 'Dana', 'Fatima', 'Gina', 'Helen', 'Isabelle', 'Jessica', 'Ivy', 'Sara']; +const LAST_NAMES = ['Allen', 'Bass', 'Dearborn', 'Flair', 'Gorman', 'Hamburg', 'Ivanas', 'James', 'Moore', 'Taylor']; +const PHONE_NUMBERS = [ + '+256414345783', '+256414345784', '+256414345785', + '+256414345786', '+256414345787', '+256414345788', + '+256414345789', '+256414345790', '+256414345791', + '+256414345792' +]; +const PATIENT_IDS = [65421, 65422, 65423, 65424, 65425, 65426, 65427, 65428, 65429, 65430]; + const getReportContext = (patient, submitter) => { const context = { - fields: - { - patient_id: patient._id, - patient_uuid: patient._id, - patient_name: patient.name, - }, + fields: { + patient_id: patient._id, + patient_uuid: patient._id, + patient_name: patient.name, + }, }; if (submitter) { context.contact = { @@ -22,22 +33,20 @@ const getReportContext = (patient, submitter) => { return context; }; -const createData = ({ healthCenter, user, nbrClinics=10, nbrPersons=10 }) => { +const createDataWithFixedData = ({ healthCenter, user, nbrClinics = 10, nbrPersons = 10 }) => { const clinics = Array .from({ length: nbrClinics }) .map((_, idx) => placeFactory.place().build({ type: 'clinic', parent: { _id: healthCenter._id, parent: healthCenter.parent }, - name: `clinic_${idx}` + name: `clinic_${idx}`, })); const persons = [ - ...clinics.map(clinic => Array - .from({ length: nbrPersons }) - .map((_, idx) => personFactory.build({ - parent: { _id: clinic._id, parent: clinic.parent }, - name: `person_${clinic.name}_${idx}`, - }))), + ...clinics.map(clinic => Array.from({ length: nbrPersons }).map((_, idx) => personFactory.build({ + parent: { _id: clinic._id, parent: clinic.parent }, + name: `person_${clinic.name}_${idx}`, + }))), ].flat(); const reports = [ @@ -51,7 +60,81 @@ const createData = ({ healthCenter, user, nbrClinics=10, nbrPersons=10 }) => { return { clinics, reports, persons }; }; -const createHierarchy = ({ name, user=false, nbrClinics=50, nbrPersons=10 }) => { +const createClinic = (index, healthCenter) => { + const firstName = FIRST_NAMES[index % FIRST_NAMES.length]; + const lastName = LAST_NAMES[index % LAST_NAMES.length]; + const personName = `${firstName} ${lastName}`; + const personPhoneNumber = PHONE_NUMBERS[index % PHONE_NUMBERS.length]; + + const primaryContact = personFactory.build({ + name: personName, + phone: personPhoneNumber + }); + + const clinic = placeFactory.place().build({ + type: 'clinic', + parent: { _id: healthCenter._id, parent: healthCenter.parent }, + name: `${personName} Family`, + contact: primaryContact + }); + + primaryContact.parent = { _id: clinic._id, parent: clinic.parent }; + + return { clinic, primaryContact }; +}; + +const createAdditionalPersons = (nbrPersons, clinic) => { + return Array + .from({ length: nbrPersons - 1 }) + .map((_, i) => { + const additionalPersonName = `${FIRST_NAMES[i % FIRST_NAMES.length]} ${LAST_NAMES[i % LAST_NAMES.length]}`; + const additionalPhoneNumber = PHONE_NUMBERS[i % PHONE_NUMBERS.length]; + return personFactory.build({ + parent: { _id: clinic._id, parent: clinic.parent }, + name: additionalPersonName, + patient_id: PATIENT_IDS[i % PATIENT_IDS.length], + phone: additionalPhoneNumber + }); + }); +}; + +const createReportsForPerson = (person, user) => { + return [ + deliveryFactory.build(getReportContext(person, user)), + pregnancyFactory.build(getReportContext(person, user)), + pregnancyVisitFactory.build(getReportContext(person, user)) + ]; +}; + +const createDataWithRealNames = ({ healthCenter, user, nbrClinics = 10, nbrPersons = 10 }) => { + const clinicsData = Array + .from({ length: nbrClinics }) + .map((_, index) => { + const { clinic, primaryContact } = createClinic(index, healthCenter); + + const additionalPersons = createAdditionalPersons(nbrPersons, clinic); + + const allPersons = [primaryContact, ...additionalPersons]; + + return { clinic, persons: allPersons }; + }); + + const allPersons = clinicsData.flatMap(data => data.persons); + const clinicList = clinicsData.map(data => data.clinic); + + const reports = allPersons.flatMap(person => createReportsForPerson(person, user)); + + return { clinics: clinicList, reports, persons: allPersons }; +}; + +const createData = ({ healthCenter, user, nbrClinics, nbrPersons, useRealNames = false }) => { + if (useRealNames) { + return createDataWithRealNames({ healthCenter, user, nbrClinics, nbrPersons }); + } + return createDataWithFixedData({ healthCenter, user, nbrClinics, nbrPersons }); +}; + +const createHierarchy = ({ name, user = false, nbrClinics = 50, nbrPersons = 10, useRealNames = false }) => { const hierarchy = placeFactory.generateHierarchy(); const healthCenter = hierarchy.get('health_center'); user = user && userFactory.build({ place: healthCenter._id, roles: ['chw'] }); @@ -61,7 +144,8 @@ const createHierarchy = ({ name, user=false, nbrClinics=50, nbrPersons=10 }) => return place; }); - const { clinics, reports, persons } = createData({ healthCenter, nbrClinics, nbrPersons, user }); + healthCenter.name = `${name}'s Area`; + const { clinics, reports, persons } = createData({ healthCenter, nbrClinics, nbrPersons, user, useRealNames }); return { user, diff --git a/tests/page-objects/default/common/common.wdio.page.js b/tests/page-objects/default/common/common.wdio.page.js index 75c23a55455..3e93b56b7de 100644 --- a/tests/page-objects/default/common/common.wdio.page.js +++ b/tests/page-objects/default/common/common.wdio.page.js @@ -18,6 +18,8 @@ const moreOptionsMenu = () => $('.more-options-menu-container>.mat-mdc-menu-trig const hamburgerMenuItemSelector = '#header-dropdown li'; const logoutButton = () => $(`${hamburgerMenuItemSelector} .fa-power-off`); const syncButton = () => $(`${hamburgerMenuItemSelector} a:not(.disabled) .fa-refresh`); +const contactsButton = () => $(hamburgerMenuItemSelector).$('//span[text()="People"]'); +const contactsTab = () => $('#contacts-tab'); const messagesTab = () => $('#messages-tab'); const analyticsTab = () => $('#analytics-tab'); const taskTab = () => $('#tasks-tab'); @@ -477,6 +479,7 @@ module.exports = { logout, logoutButton, getLogoutMessage, + contactsTab, messagesTab, analyticsTab, goToReports, @@ -490,6 +493,7 @@ module.exports = { sync, syncAndNotWaitForSuccess, syncButton, + contactsButton, closeReloadModal, goToMessages, goToTasks, @@ -501,6 +505,7 @@ module.exports = { isTargetMenuItemPresent, isTargetAggregatesMenuItemPresent, openHamburgerMenu, + closeHamburgerMenu, openAboutMenu, openUserSettingsAndFetchProperties, openUserSettings, @@ -529,5 +534,5 @@ module.exports = { goToUrl, getFastActionItemsLabels, getActionBarLabels, - getErrorLog + getErrorLog, }; diff --git a/tests/page-objects/default/reports/reports.wdio.page.js b/tests/page-objects/default/reports/reports.wdio.page.js index bc3fa516eb7..e146641e0ed 100644 --- a/tests/page-objects/default/reports/reports.wdio.page.js +++ b/tests/page-objects/default/reports/reports.wdio.page.js @@ -447,6 +447,12 @@ const verifyReport = async () => { expect(validatedReport.patient).to.be.undefined; }; +const openFirstReport = async () => { + const firstReport = leftPanelSelectors.firstReport(); + await firstReport.waitForClickable(); + await openSelectedReport(firstReport); +}; + module.exports = { leftPanelSelectors, rightPanelSelectors, @@ -490,4 +496,5 @@ module.exports = { getReportListLoadingStatus, openSelectedReport, verifyReport, + openFirstReport, }; diff --git a/tests/utils/screenshots.js b/tests/utils/screenshots.js new file mode 100644 index 00000000000..0bfe1d6fcaa --- /dev/null +++ b/tests/utils/screenshots.js @@ -0,0 +1,75 @@ +/** + * This file uses the `sharp` library for image manipulation to overcome limitations + * in WebdriverIO's native screenshot and window resizing capabilities. + * + * WebdriverIO's `setWindowSize` function has a minimum width of 500px, + * which is insufficient for capturing mobile screenshots. + * And the `takeScreenshot` captures the entire window size, not just the viewport. + * + * To address this, we capture the screenshot using WebdriverIO and then use + * `sharp` to resize the image to match the desired viewport size. This ensures + * that our screenshots accurately represent the mobile view of the application. + */ +const sharp = require('sharp'); + +const MOBILE_WINDOW_WIDTH = 768; +const MOBILE_VIEWPORT_WIDTH = 320; +const MOBILE_VIEWPORT_HEIGHT = 570; +const DESKTOP_WINDOW_WIDTH = 1000; +const DESKTOP_WINDOW_HEIGHT = 820; +const HIGH_DENSITY_DISPLAY_2X = 2; + +const isMobile = async () => { + const { width } = await browser.getWindowSize(); + return width < MOBILE_WINDOW_WIDTH; +}; + +const resizeWindowForScreenshots = async () => { + if (await isMobile()) { + return await browser.emulateDevice({ + viewport: { + width: MOBILE_VIEWPORT_WIDTH, + height: MOBILE_VIEWPORT_HEIGHT, + isMobile: true, + hasTouch: true, + }, + userAgent: 'Mozilla/5.0 (Linux; Android 11; Pixel 4) ' + + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + + 'Chrome/92.0.4515.159 Mobile Safari/537.36' + }); + } + + return await browser.setWindowSize(DESKTOP_WINDOW_WIDTH, DESKTOP_WINDOW_HEIGHT); +}; + +const generateScreenshot = async (scenario, step) => { + const device = await isMobile() ? 'mobile' : 'desktop'; + + const filename = `./tests/e2e/visual/images/${scenario}-${step}-${device}.png`; + + const newScreenshot = await browser.takeScreenshot(); + let screenshotSharp = sharp(Buffer.from(newScreenshot, 'base64')); + const metadata = await screenshotSharp.metadata(); + + const isMobileDevice = await isMobile(); + const extractWidth = isMobileDevice ? Math.min(MOBILE_VIEWPORT_WIDTH*2, metadata.width) : metadata.width; + const extractHeight = isMobileDevice ? Math.min(MOBILE_VIEWPORT_HEIGHT*2, metadata.height) : metadata.height; + screenshotSharp = screenshotSharp.extract({ + width: extractWidth, + height: extractHeight, + left: 0, + top: 0, + }); + + screenshotSharp = screenshotSharp.resize( + extractWidth * HIGH_DENSITY_DISPLAY_2X, + extractHeight * HIGH_DENSITY_DISPLAY_2X + ); + + await screenshotSharp.toFile(filename); +}; + +module.exports = { + resizeWindowForScreenshots, + generateScreenshot, +};