From 57e500eb8ae8154f4ec845008f40b14f8e5897af Mon Sep 17 00:00:00 2001 From: mildred Date: Mon, 4 Mar 2024 20:57:54 +0100 Subject: [PATCH] First commit --- .DS_Store | Bin 0 -> 14340 bytes .gitignore | 8 + README.md | 136 +++++++ package-lock.json | 91 +++++ package.json | 14 + playwright.config.ts | 77 ++++ src/.DS_Store | Bin 0 -> 6148 bytes src/constants/constants.ts | 61 +++ src/constants/credential_fields.ts | 41 ++ src/constants/env_constants.ts | 18 + src/data_stores/credentials_data_store.ts | 109 ++++++ src/data_stores/usersDataInitialization.ts | 82 ++++ src/interfaces/User.ts | 8 + src/interfaces/credential_interfaces.ts | 74 ++++ src/page-objects/AD_AddMembershipPage.ts | 152 ++++++++ .../AD_BasicUserInfoSectionInPage.ts | 125 +++++++ src/page-objects/AD_DashboardPage.ts | 66 ++++ src/page-objects/AD_ImportDataPage.ts | 123 ++++++ src/page-objects/AD_ImportTemplatePage.ts | 65 ++++ src/page-objects/AD_LeftMenuAdminPage.ts | 89 +++++ src/page-objects/AD_MembershipSectionPage.ts | 125 +++++++ src/page-objects/AD_TemplatesPage.ts | 209 +++++++++++ .../AD_UserPersonalInformationPage.ts | 108 ++++++ src/page-objects/AD_ViewImportedDataPage.ts | 155 ++++++++ src/page-objects/AD_ViewRolesPage.ts | 37 ++ src/page-objects/AD_ViewServicesPage.ts | 41 ++ src/page-objects/AD_ViewUsersPage.ts | 89 +++++ src/page-objects/COMM_LoginPage.ts | 58 +++ src/page-objects/US_LeftMenuUserPage.ts | 59 +++ src/page-objects/US_ViewCredentialPage.ts | 58 +++ src/page-objects/US_ViewMyCredentialsPage.ts | 119 ++++++ src/steps.ts | 350 ++++++++++++++++++ src/utils.ts | 131 +++++++ tests/.DS_Store | Bin 0 -> 6148 bytes tests/00-COMM-loginFunctionality.spec.ts | 38 ++ tests/01-AD-leftmenuNavigation.spec.ts | 78 ++++ tests/02-AD-manageUsers.spec.ts | 325 ++++++++++++++++ tests/03-COMM-templatesAndDataFiles.spec.ts | 310 ++++++++++++++++ tests/events_messages.json | 35 ++ vc_excel/.DS_Store | Bin 0 -> 6148 bytes vc_excel/course-credential-empty.xlsx | Bin 0 -> 8758 bytes vc_excel/course-credential.xlsx | Bin 0 -> 8758 bytes vc_excel/device-purchase-empty.xlsx | Bin 0 -> 9383 bytes vc_excel/device-purchase.xlsx | Bin 0 -> 9383 bytes vc_excel/e-operator-claim-empty.xlsx | Bin 0 -> 7956 bytes vc_excel/e-operator-claim.xlsx | Bin 0 -> 7956 bytes vc_excel/federation-membership-empty.xlsx | Bin 0 -> 8606 bytes vc_excel/federation-membership.xlsx | Bin 0 -> 8606 bytes ...ulnerability-alienUser-with-user5Data.xlsx | Bin 0 -> 12947 bytes vc_excel/financial-vulnerability-empty.xlsx | Bin 0 -> 8290 bytes ...cial-vulnerability-with-alien-columns.xlsx | Bin 0 -> 12696 bytes ...ial-vulnerability-with-required-empty.xlsx | Bin 0 -> 12507 bytes ...ulnerability-without-required-columns.xlsx | Bin 0 -> 12434 bytes vc_excel/financial-vulnerability.xlsx | Bin 0 -> 12529 bytes vc_excel/membership-card-empty.xlsx | Bin 0 -> 11954 bytes vc_excel/membership-card.xlsx | Bin 0 -> 12362 bytes 56 files changed, 3664 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 src/.DS_Store create mode 100644 src/constants/constants.ts create mode 100644 src/constants/credential_fields.ts create mode 100644 src/constants/env_constants.ts create mode 100644 src/data_stores/credentials_data_store.ts create mode 100644 src/data_stores/usersDataInitialization.ts create mode 100644 src/interfaces/User.ts create mode 100644 src/interfaces/credential_interfaces.ts create mode 100644 src/page-objects/AD_AddMembershipPage.ts create mode 100644 src/page-objects/AD_BasicUserInfoSectionInPage.ts create mode 100644 src/page-objects/AD_DashboardPage.ts create mode 100644 src/page-objects/AD_ImportDataPage.ts create mode 100644 src/page-objects/AD_ImportTemplatePage.ts create mode 100644 src/page-objects/AD_LeftMenuAdminPage.ts create mode 100644 src/page-objects/AD_MembershipSectionPage.ts create mode 100644 src/page-objects/AD_TemplatesPage.ts create mode 100644 src/page-objects/AD_UserPersonalInformationPage.ts create mode 100644 src/page-objects/AD_ViewImportedDataPage.ts create mode 100644 src/page-objects/AD_ViewRolesPage.ts create mode 100644 src/page-objects/AD_ViewServicesPage.ts create mode 100644 src/page-objects/AD_ViewUsersPage.ts create mode 100644 src/page-objects/COMM_LoginPage.ts create mode 100644 src/page-objects/US_LeftMenuUserPage.ts create mode 100644 src/page-objects/US_ViewCredentialPage.ts create mode 100644 src/page-objects/US_ViewMyCredentialsPage.ts create mode 100644 src/steps.ts create mode 100644 src/utils.ts create mode 100644 tests/.DS_Store create mode 100644 tests/00-COMM-loginFunctionality.spec.ts create mode 100644 tests/01-AD-leftmenuNavigation.spec.ts create mode 100644 tests/02-AD-manageUsers.spec.ts create mode 100644 tests/03-COMM-templatesAndDataFiles.spec.ts create mode 100644 tests/events_messages.json create mode 100644 vc_excel/.DS_Store create mode 100644 vc_excel/course-credential-empty.xlsx create mode 100644 vc_excel/course-credential.xlsx create mode 100644 vc_excel/device-purchase-empty.xlsx create mode 100644 vc_excel/device-purchase.xlsx create mode 100644 vc_excel/e-operator-claim-empty.xlsx create mode 100644 vc_excel/e-operator-claim.xlsx create mode 100644 vc_excel/federation-membership-empty.xlsx create mode 100644 vc_excel/federation-membership.xlsx create mode 100644 vc_excel/financial-vulnerability-alienUser-with-user5Data.xlsx create mode 100644 vc_excel/financial-vulnerability-empty.xlsx create mode 100644 vc_excel/financial-vulnerability-with-alien-columns.xlsx create mode 100644 vc_excel/financial-vulnerability-with-required-empty.xlsx create mode 100644 vc_excel/financial-vulnerability-without-required-columns.xlsx create mode 100644 vc_excel/financial-vulnerability.xlsx create mode 100644 vc_excel/membership-card-empty.xlsx create mode 100644 vc_excel/membership-card.xlsx diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..54ffbcfc5198db1b36f2e758f5c558dda2362c1b GIT binary patch literal 14340 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 { + await loginAsUser(page, USER1_EMAIL, URL_IDHUB); + await expect.soft(page).toHaveTitle('Dashboard – IdHub'); + }) +``` +The 'loginAs' function, is defined in `steps.ts` as follow: + +```typescript +export async function loginAsUser(page: Page, userEmail: string, url: string) { + + try { + const loginPage = new LogInPage(page); + await loginPage.visit(url); + await loginPage.login(userEmail, USER_K); + + const currentTitle = await page.title(); + + if (currentTitle === 'Data Protection – IdHub') { + // Code to accept terms and conditions + await page.click('#id_accept_privacy'); + await page.click('#id_accept_legal'); + await page.click('#id_accept_cookies'); + await page.click('a[type="button"]'); + } + await expect(page).toHaveTitle('Dashboard – IdHub'); + } catch (error) { + console.error(`Failed to login as user: `); + throw error; + } +} +``` +The 'loginAs' function in `steps.ts` use the 'LoginPage' page object, where are defined all the user related actions (e.g., visit, login, etc.). + +## Project Directory Structure + +### src directory +- **constants directory:**: Describe.... +- **data_stores directory**: Describe... +- **interfaces directory**: Describe +- **page-objects directory**: Describe +- **steps.ts**: Describe +- **utils.ts**: Describe + +### tests directory +The tests directory is where all test files are stored. These test files contain the actual tests that Playwright will execute to validate the functionality of the application. The configuration for the tests directory and other related settings are defined in the Playwright configuration file, typically named playwright.config.ts. + +### vc_excel directory +describe + + +## License +[Include information about the project's license.] + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ff7d0d5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "idhub-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "idhub-test", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.40.1", + "@types/node": "^20.10.6" + } + }, + "node_modules/@playwright/test": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz", + "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==", + "dev": true, + "dependencies": { + "playwright": "1.40.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", + "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz", + "integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==", + "dev": true, + "dependencies": { + "playwright-core": "1.40.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", + "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2d907af --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "idhub-test", + "version": "1.0.0", + "description": "Testing of IdHub modules", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "Orchestral team", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.40.1", + "@types/node": "^20.10.6" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..55b8ad7 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: false, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, +/* + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, +*/ + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..65a78948552ae9856aa836ece97969c270b5d46e GIT binary patch literal 6148 zcmeHK%Sr=55Ukc50_KpT$N2#d{=pF97x)93L<9+}5WVlo@AA`9{Xh&GFM=1ThVGi_ z?b>1Lu)PhyR`0u8UsrhsFiCt92h;+W=1_L&D!4tNV>hlTbF7S#+Jd^dE z|Axcvu-$KlVSk>gtQ3#}Qa}nw0V(hc1-$pt=2wY|Qa}nwfiDI8`_Sl)y>Lv7PX|Mc z0K^&7VO+;7L2RBN_QEld8JZ=Pm{h9~!;;Q?tGZq|CMF$L!-v($RuhWF>3n~Sa#&AP zlmb%VRDsLfF1`QX(SMl#Pf6NI0V(jW6tLO)e!b$As<%!q=e@SkALw55Nq6HqC=Ah# kiP4U^@pgP2MOoK;&F8&vObj~nK_}{Gz;%&HfxlMZ2xeUs&;S4c literal 0 HcmV?d00001 diff --git a/src/constants/constants.ts b/src/constants/constants.ts new file mode 100644 index 0000000..b399189 --- /dev/null +++ b/src/constants/constants.ts @@ -0,0 +1,61 @@ +/*Constants for TEMPLATES testing*/ + +//JSON_SCHEMA_FVC: A string representing the filename of the financial vulnerability JSON schema. +export const JSON_SCHEMA_FVC = "financial-vulnerability.json" + +//JSON_SCHEMA_ID_FVC: A URL pointing to the online location of the financial vulnerability JSON schema. +export const JSON_SCHEMA_ID_FVC = "https://idhub.pangea.org/vc_schemas/financial-vulnerability.json" + +/*Files used in DATA IMPORT testing*/ + +//PATH_FILES_TO_IMPORT: A string representing the path to the directory where Excel files are located for data import testing. +export const PATH_FILES_TO_IMPORT = '/vc_excel/' + +//Financial Vulnerability Credential +export const FILE_TO_IMPORT_FVC = 'financial-vulnerability.xlsx' +export const FILE_TO_IMPORT_FVC_EMPTY = 'financial-vulnerability-empty.xlsx' +export const FILE_TO_IMPORT_FVC_WITHOUT_REQUIRED_COLUMNS = 'financial-vulnerability-without-required-columns.xlsx' +export const FILE_TO_IMPORT_FVC_WITH_ALIEN_COLUMNS = 'financial-vulnerability-with-alien-columns.xlsx' + +//"Schema name" as appears in the Idhub User interface for FVC +export const SCHEMA_FVC = 'Financial Vulnerability Credential' + +//"Schema type" as appears in the Idhub User interface for FVC +export const SCHEMA_TYPE_FVC = 'FinancialVulnerabilityCredential' + + +//Membership Card +export const FILE_TO_IMPORT_MC = 'membership-card.xlsx'; +export const SCHEMA_MC = 'Membership Card' +export const SCHEMA_TYPE_MC = 'MembershipCardCredential' //Revisar este valor en la interfaz + +//NGOFederationMembership +export const FILE_TO_IMPORT_FM = 'federation-membership.xlsx'; +export const SCHEMA_FM = 'Federation Membership' +export const SCHEMA_TYPE_FM = 'FederationMembership' //Revisar este valor en la interfaz (en Type) + +//Course Credential +export const FILE_TO_IMPORT_CC = 'course-credential.xlsx'; +export const SCHEMA_CC = 'Course Credential' +export const SCHEMA_TYPE_CC = 'CourseCredential' //Revisar este valor en la interfaz + + +//Messages +export const ALERT_FILE_IMPORTED_SUCCESSFULLY = "The file was imported successfully!" +export const ALERT_FILE_TO_IMPORT_IS_EMPTY = "The file you try to import is empty" + +export const ALERT_FILE_IS_BADLY_FORMATTED = "This file is badly formatted!" +export const ALERT_FILE_TO_IMPORT_WITHOUT_REQUIRED_COLUMS = "line 1: 'email' is a required property" +export const ALERT_FILE_TO_IMPORT_WITH_ALIEN_COLUMS = "line 1: Additional properties are not allowed ('alien1', 'alien2' were unexpected)" +export const ALERT_USER_CREATED_SUCESSFULLY_MESSAGE = 'The account was created successfully' + +export const ERROR_INCORRECT_EMAIL_PASSWORD = 'Please enter a correct Email address and password. Note that both fields may be case-sensitive.' + + +//Memberships +export const MEMBERSHIP_TYPES = [ + { id: '1', type: 'Beneficiary' }, + { id: '2', type: 'Employee' }, + { id: '3', type: 'Member' } + ]; + diff --git a/src/constants/credential_fields.ts b/src/constants/credential_fields.ts new file mode 100644 index 0000000..39aa91e --- /dev/null +++ b/src/constants/credential_fields.ts @@ -0,0 +1,41 @@ + +export const CREDENTIAL_TYPES_FIELDS_LIST = { + "FinancialVulnerabilityCredential": [ + 'firstName', + 'lastName', + 'email', + 'phoneNumber', + 'identityDocType', + 'identityNumber', + 'streetAddress', + 'socialWorkerName', + 'socialWorkerSurname', + 'financialVulnerabilityScore', + 'amountCoveredByOtherAids', + 'connectivityOptionList', + 'assessmentDate' + ] as string[], + + + // TODO Add other type credentials... +}; + +export const CREDENTIAL_TYPES_REQUIRED_FIELDS_LIST = { + "FinancialVulnerabilityCredential": [ + "id", + "firstName", + "lastName", + "email", + "identityDocType", + "identityNumber", + "streetAddress", + "socialWorkerName", + "socialWorkerSurname", + "financialVulnerabilityScore", + "amountCoveredByOtherAids", + "assessmentDate" + ] as string[], + + + // TODO Add other type credentials... +}; \ No newline at end of file diff --git a/src/constants/env_constants.ts b/src/constants/env_constants.ts new file mode 100644 index 0000000..3f579db --- /dev/null +++ b/src/constants/env_constants.ts @@ -0,0 +1,18 @@ +/*Login*/ +export const ADMIN_EMAIL = "idhub_admin@pangea.org" +export const ADMIN_K = "1234" +export const KO_ADMIN_K = "876" +//export const URL_IDHUB = "https://idhub-autotest.demo.pangea.org" +export const URL_IDHUB = "https://idhub-nightly.demo.pangea.org" + +export const USER1_EMAIL = "user1@example.org" +export const USER2_EMAIL = "user2@example.org" +export const USER3_EMAIL = "user3@example.org" +export const USER4_EMAIL = "user4@example.org" +export const USER5_EMAIL = "user5@example.org" + +export const USER_K = "1234" +export const KO_USER_K = "876" + +export const URL_PASS_RESET = URL_IDHUB + "/auth/password_reset/" +export const URL_SCHEMAS = URL_IDHUB + "/admin/schemas/" \ No newline at end of file diff --git a/src/data_stores/credentials_data_store.ts b/src/data_stores/credentials_data_store.ts new file mode 100644 index 0000000..122e21f --- /dev/null +++ b/src/data_stores/credentials_data_store.ts @@ -0,0 +1,109 @@ +import { CourseCredential_fields, FinancialVulnerabilityCredential_fields, MembershipCard_fields, NGOFederationMembership_fields } from "../interfaces/credential_interfaces"; + +// List of entries in data store for the different types of credentials +export const CREDENTIAL_TYPES_DATA_STORE = { + FinancialVulnerabilityCredential_data: [] as FinancialVulnerabilityCredential_fields[], + MembershipCard_data: [] as MembershipCard_fields[], + NGOFederationMembership_data: [] as NGOFederationMembership_fields[], + CourseCredential_data: [] as CourseCredential_fields[], + //TODO añadir otros tipos de credenciales +}; + +//Testing data for financialVulnerabilityCredential +CREDENTIAL_TYPES_DATA_STORE.FinancialVulnerabilityCredential_data = [{ + firstName: 'Wolfgang Amadeus', + lastName: 'Mozart', + email: 'user1@example.org', + phoneNumber: '678567456', + identityDocType: 'DNI', + identityNumber: '45678900V', + streetAddress: 'C/ Tallers 1', + socialWorkerName: 'Ana', + socialWorkerSurname: 'Fernández', + financialVulnerabilityScore: '4', + amountCoveredByOtherAids: '0', + connectivityOptionList: 'Fibra', + assessmentDate: '2024-01-01' +}, +{ + firstName: 'Ludwing Van', + lastName: 'Beethoven', + email: 'user2@example.org', + phoneNumber: 'undefined', + identityDocType: 'DNI', + identityNumber: '76677667Q', + streetAddress: 'C/ Fontanals', + socialWorkerName: 'Pedro', + socialWorkerSurname: 'Ruíz', + financialVulnerabilityScore: '6', + amountCoveredByOtherAids: '30', + connectivityOptionList: 'undefined', + assessmentDate: '2024-01-03' +}, +{ + firstName: 'Franz', + lastName: 'Schubert', + email: 'user3@example.org', + phoneNumber: '609897645', + identityDocType: 'DNI', + identityNumber: '77777777A', + streetAddress: 'C/Miro', + socialWorkerName: 'Maria', + socialWorkerSurname: 'Sánchez', + financialVulnerabilityScore: '8', + amountCoveredByOtherAids: '10', + connectivityOptionList: 'Fibra', + assessmentDate: '2023-10-04' +}, +{ + firstName: 'Barbara', + lastName: 'Strozzi', + email: 'user4@example.org', + phoneNumber: '654899876', + identityDocType: 'NIE', + identityNumber: '987876765X', + streetAddress: 'C/ Picasso', + socialWorkerName: 'Claudia', + socialWorkerSurname: 'Puig', + financialVulnerabilityScore: '9', + amountCoveredByOtherAids: '0', + connectivityOptionList: 'Fibra', + assessmentDate: '2024-01-04' +} +]; + +CREDENTIAL_TYPES_DATA_STORE.MembershipCard_data = [ + //TODO: fill with data +]; + +CREDENTIAL_TYPES_DATA_STORE.NGOFederationMembership_data = [ + //TODO: fill with data +]; +CREDENTIAL_TYPES_DATA_STORE.CourseCredential_data = [ + //TODO: fill with data +]; + + + +export const CREDENTIAL_TYPE_DATASTORE_UNDEFINED = { + FinancialVulnerabilityCredential_data_undefined: {} as FinancialVulnerabilityCredential_fields + //TODO añadir otros tipos de credenciales +} + + +CREDENTIAL_TYPE_DATASTORE_UNDEFINED.FinancialVulnerabilityCredential_data_undefined = { + firstName: 'undefined_data', + lastName: 'undefined_data', + email: 'undefined_data', + phoneNumber: 'undefined_data', + identityDocType: 'undefined_data', + identityNumber: 'undefined_data', + streetAddress: 'undefined_data', + socialWorkerName: 'undefined_data', + socialWorkerSurname: 'undefined_data', + financialVulnerabilityScore: 'undefined_data', + amountCoveredByOtherAids: 'undefined_data', + connectivityOptionList: 'undefined_data', + assessmentDate: 'undefined_data', +} + diff --git a/src/data_stores/usersDataInitialization.ts b/src/data_stores/usersDataInitialization.ts new file mode 100644 index 0000000..5c00f20 --- /dev/null +++ b/src/data_stores/usersDataInitialization.ts @@ -0,0 +1,82 @@ +import { User } from "../interfaces/User"; + +export const users: User[] = [ + { + firstName: 'John', + lastName: 'Smith', + email: 'john.smith@example.org', + membershipType: '1', + startDate: '2023-01-01', + endDate: '2024-01-01' + }, + { + firstName: 'David', + lastName: 'Roberts', + email: 'david.roberts@example.org', + membershipType: '2', + startDate: '2022-01-01', + endDate: '2025-01-01', + }, + { + firstName: 'Julia', + lastName: 'Pierce', + email: 'julia.pierce@example.org', + membershipType: '3', + startDate: '2023-01-01', + endDate: '2025-01-01' + }, + { + firstName: 'Laura', + lastName: 'Rey', + email:'laura.rey@example.org', + membershipType: '3', + startDate: '2021-01-01', + endDate: '2025-01-01' + } + +]; + +export const testingUsers: User[] = [ + { + firstName: 'Wolfgang Amadeus', + lastName: 'Mozart', + email: 'user1@example.org', + membershipType: '1', + startDate: '2023-01-01', + endDate: '2024-01-01' + }, + { + firstName: 'Ludwing Van', + lastName: 'Beethoven', + email: 'user2@example.org', + membershipType: '2', + startDate: '2022-01-01', + endDate: '2025-01-01', + }, + { + firstName: 'Franz', + lastName: 'Schubert', + email: 'user3@example.org', + membershipType: '3', + startDate: '2023-01-01', + endDate: '2025-01-01' + }, + { + firstName: 'Barbara', + lastName: 'Strozzi', + email:'user4@example.org', + membershipType: '1', + startDate: '2021-01-01', + endDate: '2025-01-01' + }, + + { + firstName: 'Clara', + lastName: 'Schumann', + email:'user5@example.org', + membershipType: '3', + startDate: '2021-01-01', + endDate: '2025-01-01' + } + +]; \ No newline at end of file diff --git a/src/interfaces/User.ts b/src/interfaces/User.ts new file mode 100644 index 0000000..374ad27 --- /dev/null +++ b/src/interfaces/User.ts @@ -0,0 +1,8 @@ +export interface User { + firstName: string; + lastName: string; + email: string; + membershipType: string; + startDate: string; + endDate: string; +} \ No newline at end of file diff --git a/src/interfaces/credential_interfaces.ts b/src/interfaces/credential_interfaces.ts new file mode 100644 index 0000000..c82ed90 --- /dev/null +++ b/src/interfaces/credential_interfaces.ts @@ -0,0 +1,74 @@ +export interface FinancialVulnerabilityCredential_fields { + firstName: string; + lastName: string; + email: string; + phoneNumber: string; + identityDocType: string + identityNumber: string; + streetAddress: string; + socialWorkerName: string; + socialWorkerSurname: string; + financialVulnerabilityScore: string; + amountCoveredByOtherAids: string; + connectivityOptionList: string; + assessmentDate: string; + } + + export interface MembershipCard_fields { + organisation: string; + membershipType: string; + membershipId: string; + affiliatedSince: string; + affiliatedUntil: string; + typeOfPerson: 'natural' | 'legal'; + identityDocType: string; + identityNumber: string; + firstName: string; + lastName: string; + role: string; + email: string; + } + + export interface NGOFederationMembership_fields { + federation: string; + legalName: string; + shortName: string; + registrationIdentifier: string; + publicRegistry: string; + streetAddress: string; + postCode: string; + city: string; + taxReference: string; + membershipType: string; + membershipStatus: string; + membershipId: string; + membershipSince: string; // string in YYYY-MM-DD format + email: string; + phone: string; + website: string; + evidence: string; + certificationDate: string; // string in YYYY-MM-DD format + } + + export interface CourseCredential_fields { + firstName: string; + lastName: string; + email: string; + personalIdentifier: string; + issuedDate: string; // a string in YYYY-MM-DD format + modeOfInstruction: string; + courseDuration: number; + courseDays: number; + courseName: string; + courseDescription: string; + gradingScheme: string; + scoreAwarded: number; + qualificationAwarded: "A+" | "A" | "B" | "C" | "D"; + courseLevel: string; + courseFramework: string; + courseCredits: number; + dateOfAssessment: string; // a string in YYYY-MM-DD format + evidenceAssessment: string; + } + + //TODO complete \ No newline at end of file diff --git a/src/page-objects/AD_AddMembershipPage.ts b/src/page-objects/AD_AddMembershipPage.ts new file mode 100644 index 0000000..3f30a9a --- /dev/null +++ b/src/page-objects/AD_AddMembershipPage.ts @@ -0,0 +1,152 @@ +import { Page, Locator } from "@playwright/test" +import { ALERT_USER_CREATED_SUCESSFULLY_MESSAGE } from "../constants/constants" + +export class AddMembershipPage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly typeOfMembership: Locator + readonly startDate: Locator + readonly endDate: Locator + readonly saveButton: Locator + + readonly cancelButton: Locator + readonly addMembershipButton: Locator + + public constructor(page: Page) { + this.page = page; + this.primaryTitle = this.page.getByRole('heading', { name: 'User management' }) + this.secondaryTitle = this.page.getByRole('heading', { name: 'Associate a membership to the user' }) + this.typeOfMembership = this.page.getByLabel('Type of membership') + this.startDate = this.page.getByPlaceholder('Start date') + this.endDate = this.page.getByPlaceholder('End date') + this.saveButton = this.page.getByRole('button', { name: 'Save' }) + this.cancelButton = this.page.getByRole('link', { name: 'Cancel' }) + } + + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + + async getTypeOfMembership() { + try { + return this.typeOfMembership; + } catch (error) { + console.error("Failed to get Type of membership:", error); + throw error; + } + } + + async getStartDate() { + try { + return this.startDate; + } catch (error) { + console.error("Failed to get Start date:", error); + throw error; + } + } + + async getEndDate() { + try { + return this.endDate; + } catch (error) { + console.error("Failed to get End date:", error); + throw error; + } + } + + async selectSaveButton() { + try { + await this.saveButton.click() + } catch (error) { + console.error("Failed to select Save button:", error); + throw error; + } + } + + async selectCancelButton() { + try { + await this.cancelButton.click() + } catch (error) { + console.error("Failed to select Cancel button:", error); + throw error; + } + } + + async addUserMembership(membershipType: string, startDate: string, endDate: string) { + + try { + await this.page.locator('alert alert-success alert-dismissible fade show mt-3').isVisible() + await this.typeOfMembership.selectOption(membershipType) + await this.startDate.fill(startDate); + await this.endDate.fill(endDate); + await this.saveButton.click(); + } catch (error) { + console.error("Failed to add user membership:", error); + throw error; + } + } + + async addNewUserMembership(membershipType: string, startDate: string, endDate: string) { + + try { + await this.typeOfMembership.selectOption(membershipType) + await this.startDate.fill(startDate); + await this.endDate.fill(endDate); + await this.saveButton.click(); + } catch (error) { + console.error("Failed to add a new user membership:", error); + throw error; + } + } + + + async updateExistingUserMembership(page: Page, membershipToChange: string, newMembership: string, newStartDate: string, newEndDate: string) { + + try { + await this.typeOfMembership.selectOption(newMembership); + await this.startDate.fill(newStartDate); + await this.endDate.fill(newEndDate); + await this.saveButton.click(); + } catch (error) { + console.error("Failed to update a user membership:", error); + throw error; + } + } + + async alertUserCreationMessageIsValid(): Promise { + + await this.page.waitForSelector('.alert.alert-success.alert-dismissible'); + const element = await this.page.$('.alert.alert-success.alert-dismissible'); + + if (element !== null) { + const isVisible = await element.isVisible(); + if (isVisible) { + const text = await element.innerText(); + console.log(text); + if (text === ALERT_USER_CREATED_SUCESSFULLY_MESSAGE) { + return true; + } + } + } + + return false; + } + +} + diff --git a/src/page-objects/AD_BasicUserInfoSectionInPage.ts b/src/page-objects/AD_BasicUserInfoSectionInPage.ts new file mode 100644 index 0000000..9e3d220 --- /dev/null +++ b/src/page-objects/AD_BasicUserInfoSectionInPage.ts @@ -0,0 +1,125 @@ + +import { Page, Locator } from "@playwright/test"; + +export class BasicUserInfoSectionPage { + readonly page: Page; + readonly primaryTitle: Locator; + readonly secondaryTitle: Locator; + readonly firstName: Locator; + readonly lastName: Locator; + readonly email: Locator; + readonly isAdminCheckbox: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + + constructor(page: Page) { + this.page = page; + this.primaryTitle = this.page.getByRole('heading', { name: 'User management' }) + this.secondaryTitle = this.page.getByRole('heading', { name: ' Add user' }) + this.firstName = this.page.locator('input[placeholder="First name"]') + this.lastName = this.page.locator('input[placeholder="Last name"]') + this.email = this.page.locator('input[placeholder="Email address"]') + this.isAdminCheckbox = this.page.locator('#id_is_admin') + this.saveButton = this.page.getByRole('button', { name: 'Save' }) + this.cancelButton = this.page.getByRole('link', { name: 'Cancel' }) + } + + async getPrimaryTitleText() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title text:", error); + throw error; + } + } + + async getSecondaryTitleText() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title text:", error); + throw error; + } + } + + async getFirstName() { + try { + return await this.firstName.inputValue(); + } catch (error) { + console.error("Failed to get first name:", error); + throw error; + } + } + + async getLastName() { + try { + return await this.lastName.inputValue(); + } catch (error) { + console.error("Failed to get last name:", error); + throw error; + } + } + + async getEmail() { + try { + return await this.email.inputValue(); + } catch (error) { + console.error("Failed to get email:", error); + throw error; + } + } + + async checkIsAdmin() { + try { + await this.isAdminCheckbox.check(); + } catch (error) { + console.error("Failed to check 'is admin' checkbox:", error); + throw error; + } + } + + async uncheckIsAdmin() { + try { + await this.isAdminCheckbox.uncheck(); + } catch (error) { + console.error("Failed to uncheck 'is admin' checkbox:", error); + throw error; + } + } + + async addUserBasicInfo(newFirstname: string, newLastname: string, newEmail: string, isAdmin: boolean) { + try { + console.log("Adding user with email: ", newEmail); + await this.firstName.fill(newFirstname); + await this.lastName.fill(newLastname); + await this.email.fill(newEmail); + if (isAdmin) { + await this.checkIsAdmin(); + } else { + await this.uncheckIsAdmin(); + } + await this.saveButton.click(); + } catch (error) { + console.error("Failed to add user basic info:", error); + throw error; + } + } + + async updateUserBasicInfo(newFirstname: string, newLastname: string, newEmail: string, isAdmin: boolean) { + try { + console.log("Modifying user with email: ", newEmail); + await this.firstName.fill(newFirstname); + await this.lastName.fill(newLastname); + await this.email.fill(newEmail); + if (isAdmin) { + await this.checkIsAdmin(); + } else { + await this.uncheckIsAdmin(); + } + await this.saveButton.click(); + } catch (error) { + console.error("Failed to update user basic info:", error); + throw error; + } + } +} diff --git a/src/page-objects/AD_DashboardPage.ts b/src/page-objects/AD_DashboardPage.ts new file mode 100644 index 0000000..2995197 --- /dev/null +++ b/src/page-objects/AD_DashboardPage.ts @@ -0,0 +1,66 @@ +import { Page, Locator, expect } from "@playwright/test" + +export class DashboardPage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly eventTitle: Locator + readonly descriptionTitle: Locator + readonly dateTitle: Locator + + constructor(page: Page) { + this.page = page; + this.primaryTitle = page.getByRole('heading', { name: 'Dashboard' }) + this.secondaryTitle = page.getByRole('heading', { name: 'Events' }) + this.eventTitle = page.getByRole('link', { name: 'Event' }) + this.descriptionTitle = page.getByRole('link', { name: 'Event' }) + this.dateTitle = page.getByRole('link', { name: 'Date' }) + } + + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + + async getEventTitle() { + try { + return this.eventTitle; + } catch (error) { + console.error("Failed to get event title:", error); + throw error; + } + } + + async getDescriptionTitle() { + try { + return this.descriptionTitle; + } catch (error) { + console.error("Failed to get description title:", error); + throw error; + } + } + + async getDateTitle() { + try { + return this.dateTitle; + } catch (error) { + console.error("Failed to get date title:", error); + throw error; + } + } + +} \ No newline at end of file diff --git a/src/page-objects/AD_ImportDataPage.ts b/src/page-objects/AD_ImportDataPage.ts new file mode 100644 index 0000000..a5965f3 --- /dev/null +++ b/src/page-objects/AD_ImportDataPage.ts @@ -0,0 +1,123 @@ +import { Page, Locator, expect } from "@playwright/test" + +export class ImportDataPage { + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly dropdownDID: Locator + readonly dropdownSchema: Locator + readonly fileToImport: Locator + readonly saveButton: Locator + readonly cancelButton: Locator + + public constructor(page: Page) { + this.page = page; + this.primaryTitle = this.page.getByRole('heading', { name: 'Data file management' }) + this.secondaryTitle = this.page.getByRole('heading', { name: 'Import' }) + //this.dropdownDID = this.page.locator('label[for="id_did"]') + //this.dropdownSchema = this.page.locator('label[for="id_schema"]') + this.dropdownDID = this.page.getByLabel('Did') + this.dropdownSchema = this.page.getByLabel('Schema') + this.fileToImport = this.page.getByLabel('File to import') + this.saveButton = this.page.getByRole('button', { name: 'Save' }) + this.cancelButton = this.page.getByRole('link', { name: 'Cancel' }) + + } + + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + + async getDropdownDID() { + try { + return this.dropdownDID; + } catch (error) { + console.error("Failed to get dropdown DID value:", error); + throw error; + } + } + + async getDropdownSchema() { + try { + return this.dropdownSchema; + } catch (error) { + console.error("Failed to get dropdown Schema value:", error); + throw error; + } + } + + async getFileToImport() { + try { + return this.fileToImport; + } catch (error) { + console.error("Failed to get file to import value:", error); + throw error; + } + } + + async getSaveButton() { + try { + return this.saveButton; + } catch (error) { + console.error("Failed to get the 'Save' button:", error); + throw error; + } + } + + async getCancelButton() { + try { + return this.cancelButton; + } catch (error) { + console.error("Failed to get the 'Cancel' button:", error); + throw error; + } + } + async importFile(schema: string, fileToImport: string, did: string) { + try { + console.log('Importing the file from the current working directory: ${process.cwd()}'); + await (await this.getDropdownDID()).selectOption({ label: did }); + await (await this.getDropdownSchema()).selectOption({ label: schema }); + await (await this.getFileToImport()).click(); + await (await this.getFileToImport()).setInputFiles(process.cwd() + fileToImport); + await (await this.getSaveButton()).click(); + // await (await this.getCancelButton()).click(); + } catch (error) { + console.error("Failed to import file:", error); + throw error; + } + } + + async alertFileImportedUnsuccessfully(expectedMessage: string): Promise { + try { + const element = this.page.locator('.alert.alert-danger.alert-dismissible'); + if (element) { + const isVisible = await element.isVisible(); + if (isVisible) { + const text = await element.innerText(); + console.log(text); + expect(text).toBe(expectedMessage); + } + } + return false; + } catch (error) { + console.error("Failed to check for unsuccessful file import alert:", error); + throw error; + } + } + +} + diff --git a/src/page-objects/AD_ImportTemplatePage.ts b/src/page-objects/AD_ImportTemplatePage.ts new file mode 100644 index 0000000..0d7edb6 --- /dev/null +++ b/src/page-objects/AD_ImportTemplatePage.ts @@ -0,0 +1,65 @@ +import { Page, Locator } from "@playwright/test" + +export class ImportTemplatePage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly availableTemplatesTitle: Locator + readonly importTemplateButton: Locator + + constructor(page: Page) { + this.page = page; + this.primaryTitle = page.getByRole('heading', { name: 'Template management' }); + this.secondaryTitle = page.getByRole('heading', { name: 'Import template' }); + this.availableTemplatesTitle = page.getByRole('heading', { name: 'Available templates' }); + } + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + + + async gotoImportTemplate(template: String) { + try { + const row = this.page.locator(`tr:has-text('${template}')`); + await row.locator('i.bi.bi-plus-circle').click(); + } catch (error) { + console.error("Failed trying to load the import schema page:", error); + throw error; + } + } + + async schemaIsAvailableToImport(schemaName: string): Promise { + try { + console.log(`Checking if template ${schemaName} exists`); + + // Wait for the table to appear and contain at least one row + await this.page.waitForFunction(() => document.querySelectorAll('tr').length > 0, { timeout: 10000 }); + + const templateExists = await this.page.$$eval('td:nth-child(2)', (tds, schemaName) => { + return tds.some(td => (td as HTMLElement).innerText === schemaName); + }, schemaName); + + return templateExists; + + } catch (error) { + console.error("Failed checking if the schema is available to import:", error); + throw error; + } + } + +} \ No newline at end of file diff --git a/src/page-objects/AD_LeftMenuAdminPage.ts b/src/page-objects/AD_LeftMenuAdminPage.ts new file mode 100644 index 0000000..6dba4c2 --- /dev/null +++ b/src/page-objects/AD_LeftMenuAdminPage.ts @@ -0,0 +1,89 @@ +import { Page, Locator } from "@playwright/test" + +export class LeftMenuAdminPage { + readonly page: Page; + readonly dashboardLink: Locator + readonly usersLink: Locator + readonly viewUsersLink: Locator + readonly addUsersLink: Locator + readonly rolesLink: Locator + readonly manageRolesLink: Locator + readonly manageServicesLink: Locator + readonly credentialsLink: Locator + readonly viewCredentialsLink: Locator + readonly organisationWalletLink: Locator + readonly templatesLink: Locator + readonly dataLink: Locator + + + public constructor(page: Page) { + this.page = page; + this.dashboardLink = this.page.getByRole('link', { name: ' Dashboard' }) + this.usersLink = this.page.getByRole('link', { name: ' Users' }) + this.rolesLink = this.page.getByRole('link', { name: ' Roles' }) + this.credentialsLink = this.page.getByRole('link', { name: ' Credentials' }) + this.templatesLink = this.page.getByRole('link', { name: ' Templates' }) + this.dataLink = this.page.getByRole('link', { name: ' Data' }) + this.addUsersLink = this.page.getByRole('link', { name: 'Add user'}) + this.viewUsersLink = this.page.getByRole('link', { name: 'View users' }) + this.manageRolesLink = this.page.getByRole('link', { name: 'Manage roles' }) + this.manageServicesLink = this.page.getByRole('link', { name: 'Manage services' }) + this.credentialsLink = this.page.getByRole('link', { name: 'Credentials' }) + this.viewCredentialsLink = this.page.getByRole('link', { name: 'View credentials' }) + this.organisationWalletLink = this.page.getByRole('link', { name: 'View credentials' }) + this.templatesLink = this.page.getByRole('link', { name: 'Templates' }) + this.dataLink = this.page.getByRole('link', { name: ' Data' }) + } + + async getDashboardLink() { + return this.dashboardLink + } + + async getUsersLink() { + return this.usersLink + } + + async getAddUserLink() { + return this.addUsersLink + } + + async getViewUsersLink() { + return this.viewUsersLink + } + + async getRolesLink() { + return this.rolesLink + } + + async getManageRolesLink() { + return this.manageRolesLink + } + + async getManageServicesLink() { + return this.manageServicesLink + } + + async getCredentialsLink() { + return this.credentialsLink + } + + async getViewCredentialsLink() { + return this.viewCredentialsLink + } + + async getOrganisationalWalletLink() { + return this.organisationWalletLink + } + + async getTemplatesLink() { + return this.templatesLink + } + + async getDataLink() { + return this.dataLink + } + + async getDashboardLinkLocator(){ + return '.admin.nav-link[href="/admin/dashboard/"]'; + } +} \ No newline at end of file diff --git a/src/page-objects/AD_MembershipSectionPage.ts b/src/page-objects/AD_MembershipSectionPage.ts new file mode 100644 index 0000000..a4d76ff --- /dev/null +++ b/src/page-objects/AD_MembershipSectionPage.ts @@ -0,0 +1,125 @@ +import { Page, Locator } from "@playwright/test"; +import { getMembershipTypeById } from "../utils"; + +export class MembershipSectionPage { + readonly page: Page; + readonly membershipTitle: Locator; + readonly fromTitle: Locator; + readonly toTitle: Locator; + readonly addMembershipButton: Locator; + + constructor(page: Page) { + this.page = page; + this.membershipTitle = page.getByRole('button', { name: 'Membership' }); + this.fromTitle = page.getByRole('button', { name: 'From' }); + this.toTitle = page.getByRole('button', { name: 'To' }); + this.addMembershipButton = this.page.getByRole('link', { name: 'Add membership' }) + } + + async getMembershipTitle() { + try { + return this.membershipTitle; + } catch (error) { + console.error("Failed to get Membership title:", error); + throw error; + } + } + + async getFromTitle() { + try { + return this.fromTitle; + } catch (error) { + console.error("Failed to getFrom title:", error); + throw error; + } + } + + async getToTitle() { + try { + return this.toTitle; + } catch (error) { + console.error("Failed to get To title:", error); + throw error; + } + } + + async getAddMembershipButton() { + try { + return this.addMembershipButton; + } catch (error) { + console.error("Failed to get Add Membership button:", error); + throw error; + } + } + + async selectAddMembershipButton() { + try { + return (await this.getAddMembershipButton()).click(); + } catch (error) { + console.error("Failed to click Add Membership button:", error); + throw error; + } + } + + async membershipExist(page: Page, membershipType: string): Promise { + try { + const ms = getMembershipTypeById(membershipType); + const membershipExists = await page.$$eval('tr', (rows, ms) => { + // Find the row with membershipType + const membershipRow = rows.find(row => Array.from(row.children).some((child: Node & ChildNode) => (child as HTMLElement).innerText === ms)); + return !!membershipRow; + }, ms); + return membershipExists; + + } catch (error) { + console.error("The selected membershipType does not exist: ", error); + throw error; + } + } + + + async gotoModifyMembershipPage(page: Page, membershipToChange: string) { + try { + const ms = getMembershipTypeById(membershipToChange); + await page.$$eval('tr', (rows, ms) => { + // Find the row with membershipType + const membershipRow = rows.find(row => Array.from(row.children).some((child: Node & ChildNode) => (child as HTMLElement).innerText === ms)); + + // Get the "Edit" button from the row and click it + if (membershipRow) { + const editButton = membershipRow.querySelector('a[title="Edit"]') as HTMLElement; + if (editButton) { + editButton.click(); + } + } else { + throw new Error(`Membership row with membershipType ${ms} not found.`); + } + }, ms); + + } catch (error) { + console.error("The selected membershipType does not exist: ", error); + } + } + + async gotoDeleteSpecificMembership(page: Page, membershipType: string) { + try { + const ms = getMembershipTypeById(membershipType) + await page.$$eval('tr', (rows, ms) => { + // Find the row with membershipType + const membershipRow = rows.find(row => Array.from(row.children).some((child: Node & ChildNode) => (child as HTMLElement).innerText === ms)); + + // Get the "Delete" button from the row and click it + if (membershipRow) { + const deleteButton = membershipRow.querySelector('a[title="Delete"]') as HTMLElement; + if (deleteButton) { + deleteButton.click(); + } + } else { + throw new Error(`Membership row with membershipType ${ms} not found.`); + } + }, ms); + } catch (error) { + console.error("The selected membershipType does not exist: ", error); + } + } +} diff --git a/src/page-objects/AD_TemplatesPage.ts b/src/page-objects/AD_TemplatesPage.ts new file mode 100644 index 0000000..85bbc6d --- /dev/null +++ b/src/page-objects/AD_TemplatesPage.ts @@ -0,0 +1,209 @@ +import { Page, Locator, expect } from "@playwright/test" +import { clickViewSchemaOnLeftMenu } from "../steps" +import { URL_SCHEMAS } from "../constants/env_constants" + +export class TemplatesPage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly createdTitle: Locator + readonly fileSchemaTitle: Locator + readonly nameTitle: Locator + readonly descriptionTitle: Locator + readonly viewSchema: Locator + readonly deleteSchema: Locator + readonly addTemplateButton: Locator + + + constructor(page: Page) { + this.page = page; + this.primaryTitle = page.getByRole('heading', { name: 'Template management' }); + this.secondaryTitle = page.getByRole('heading', { name: 'View credential templates' }); + this.createdTitle = page.getByRole('button', { name: 'Created' }) + this.fileSchemaTitle = page.getByRole('button', { name: 'File schema' }) + this.nameTitle = page.getByRole('button', { name: 'Name' }) + this.descriptionTitle = page.getByRole('button', { name: 'Description' }) + this.addTemplateButton = page.getByRole('link', { name: 'Add template ' }) + + } + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + + async getCreatedTitle() { + try { + return this.createdTitle; + } catch (error) { + console.error("Failed to get Created title:", error); + throw error; + } + } + + async getFileSchemaTitle() { + try { + return this.getFileSchemaTitle; + } catch (error) { + console.error("Failed to get File schema title:", error); + throw error; + } + } + + async getNameTitle() { + try { + return this.nameTitle; + } catch (error) { + console.error("Failed to get Name title:", error); + throw error; + } + } + + async getDescriptionTitle() { + try { + return this.descriptionTitle; + } catch (error) { + console.error("Failed to get Description title:", error); + throw error; + } + } + + async getaddTemplateButton() { + try { + return this.addTemplateButton; + } catch (error) { + console.error("Failed to get add template button", error); + throw error; + } + } + + async checkSchemaNamesAgainstCorrespondingJSON(page: Page): Promise { + + try { + // Get the count of table rows + const rowCount = await page.locator('table tbody tr').count(); + + for (let i = 0; i < rowCount; i++) { + // Get the second cell (file schema name) of the current row + console.log("Checking schema name against JSON for element: ", i); + const fileSchemaCell = page.locator(`table tbody tr:nth-child(${i + 1}) td:nth-child(2)`); + const fileSchemaValue = await fileSchemaCell.innerText(); + + // Print the value of the second cell + console.log('Name of the schema: ' + fileSchemaValue); + + // Click the corresponding element in the fifth cell (eye icon) + await clickViewSchemaOnLeftMenu(page, fileSchemaValue) + + // Parse the JSON content of the page + const jsonContent = await page.evaluate(() => JSON.parse(document.body.innerText)); + + // Extract the last component of the $id path + const idUrl = new URL(jsonContent["$id"]); + const lastPathSegment = idUrl.pathname.split("/").pop(); + console.log('lastPathSegment', lastPathSegment); + + // Check if the last component of the $id path matches the second cell value (File schema) + expect(lastPathSegment).toBe(fileSchemaValue); + await page.goto(URL_SCHEMAS); + } + return true; + } catch (error) { + console.error("Failed checking the schema against json:", error); + throw error; + } + } + + async schemaIsAvailableInView(schemaName: string): Promise { + try { + console.log('Checking if template ${schemaName} exists'); + + // Wait for the table to appear and contain at least one row + await this.page.waitForFunction(() => document.querySelectorAll('tr').length > 0, { timeout: 10000 }); + + const templateExists = await this.page.$$eval('td:nth-child(2)', (tds, schemaName) => { + return tds.some(td => (td as HTMLElement).innerText === schemaName); + }, schemaName); + return templateExists; + } catch (error) { + console.error("A problem while loading the list of templates.", error); + throw error; + } + } + + async gotoViewSchemaPage(template: String) { + + try { + const row = this.page.locator(`tr:has-text('${template}')`); + await row.locator('i.bi.bi-eye').click(); + } catch (error) { + console.error("Failed to go to the View schemas page", error); + throw error; + } + } + + async gotoAddTemplatePage() { + try { + await this.addTemplateButton.click(); + } catch (error) { + console.error("Failed to add the template: ", error); + throw error; + } + + } + + + async gotoDeleteAndConfirmInModal(template: String) { + try { + const row = this.page.locator(`tr:has-text('${template}')`); + await row.locator('i.bi.bi-trash').click(); + + //Find the modal that corresponds to the specific template (see html) + const modal = this.page.locator(`div.modal:has-text("${template}")`) + .first(); + + // Ensure the modal is visible before trying to interact with it + await expect(modal).toBeVisible(); + + // Click the delete button within the modal + await modal.locator('.btn.btn-danger').click(); + + } catch (error) { + console.error("A problem while trying to delete the template.", error); + throw error; + } + } + + async gotoDeleteAndCancelInModal(template: String) { + try { + const row = this.page.locator(`tr:has-text('${template}')`); + await row.locator('i.bi.bi-trash').click(); + + //Find the modal that corresponds to the specific template (see html) + const modal = this.page.locator(`div.modal:has-text("${template}")`) + .first(); + + // Ensure the modal is visible before trying to interact with it + await expect(modal).toBeVisible(); + + // Click the delete button within the modal + await modal.locator('.btn.btn-secondary').click(); + } catch (error) { + console.error("A problem while trying to delete the template.", error); + throw error; + } + } +} \ No newline at end of file diff --git a/src/page-objects/AD_UserPersonalInformationPage.ts b/src/page-objects/AD_UserPersonalInformationPage.ts new file mode 100644 index 0000000..f3ef16b --- /dev/null +++ b/src/page-objects/AD_UserPersonalInformationPage.ts @@ -0,0 +1,108 @@ +import { Page, Locator } from "@playwright/test" +import { User } from '../interfaces/User' +import { formatDate, getMembershipTypeById } from "../utils" + + +export class UserPersonalInformationPage { + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly modifyButton: Locator + readonly deactivateButton: Locator + readonly deleteButton: Locator + + public constructor(page: Page) { + this.page = page; + + this.primaryTitle = page.getByRole('heading', { name: 'User management' }); + this.secondaryTitle = page.getByRole('heading', { name: ' User personal information' }); + this.modifyButton = page.getByRole('link', { name: 'Modify' }) + this.deactivateButton = page.getByRole('link', { name: 'Deactivate' }) + this.deleteButton = page.getByRole('link', { name: 'Delete' }) + } + + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + + async isTheUserInfoValid(user: User): Promise { + + try { + const firstNameElement = await this.page.waitForSelector('.col-9.text-secondary', { timeout: 6000 }); + const fn = firstNameElement ? await this.page.evaluate(el => (el as HTMLElement).innerText, firstNameElement) : ''; + + const lastNameElement = await this.page.waitForSelector('.card-body > div:nth-of-type(2) > .col-9.text-secondary', { timeout: 6000 }); + const ln = lastNameElement ? await this.page.evaluate(el => (el as HTMLElement).innerText, lastNameElement) : ''; + + const emailElement = await this.page.waitForSelector('.card-body > div:nth-of-type(3) > .col-9.text-secondary', { timeout: 6000 }); + const em = emailElement ? await this.page.evaluate(el => (el as HTMLElement).innerText, emailElement) : ''; + + const membershipTypeElement = await this.page.$$eval('tr > td:nth-of-type(1)', elements => elements.map(el => el.textContent ? el.textContent.trim() : '')); + const mst = membershipTypeElement[0]; + + const fromDateElement = await this.page.$$eval('tr > td:nth-of-type(2)', elements => elements.map(el => el.textContent ? el.textContent.trim() : '')); + const fromDate = fromDateElement[0]; + + const toDateElement = await this.page.$$eval('tr > td:nth-of-type(3)', elements => elements.map(el => el.textContent ? el.textContent.trim() : '')); + const toDate = toDateElement[0]; + + console.log(fn, ' ', ln, ' ', em, ' ', mst, ' ', fromDate, ' ', toDate); + console.log(user.firstName, ' ', user.lastName, ' ', user.email, ' ', getMembershipTypeById(user.membershipType), ' ', formatDate(user.startDate), ' ', formatDate(user.endDate)); + + if (user.firstName === fn && user.lastName === ln && user.email === em && getMembershipTypeById(user.membershipType) === mst && formatDate(user.startDate) === fromDate && formatDate(user.endDate) === toDate) { + return true + } + else { return false } + } catch (error) { + console.error("Failed to retrieve user information:", error); + throw error; + } + } + + async selectModifyUser() { + try { + await this.modifyButton.click({ timeout: 60000 }) + } catch (error) { + console.error("Failed to click the Modify button:", error); + throw error; + } + } + + async selectDeleteUser() { + try { + await this.deleteButton.click(); + } catch (error) { + console.error("Failed to click the Delete button:", error); + throw error; + } + } + + async deleteUser(page: Page) { + try { + await this.deleteButton.click(); + await this.page.waitForSelector('.modal-body', { timeout: 5000 }); + await this.page.click('a.btn.btn-danger');//delete + //await page.click('button.btn.btn-secondary');//cancel + } catch (error) { + console.error("Failed to delete the user:", error); + throw error; + } + } + +} + + diff --git a/src/page-objects/AD_ViewImportedDataPage.ts b/src/page-objects/AD_ViewImportedDataPage.ts new file mode 100644 index 0000000..c231fa3 --- /dev/null +++ b/src/page-objects/AD_ViewImportedDataPage.ts @@ -0,0 +1,155 @@ +import { Page, Locator, expect } from "@playwright/test" +import { ALERT_FILE_IMPORTED_SUCCESSFULLY } from "../constants/constants" + +export class ViewImportedDataPage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly dateTitle: Locator + readonly fileTitle: Locator + readonly successTitle: Locator + readonly importDataButton: Locator + + constructor(page: Page) { + this.page = page; + this.primaryTitle = page.getByRole('heading', { name: 'Data file management' }); + this.secondaryTitle = page.getByRole('heading', { name: 'Import data' }); + this.dateTitle = page.getByRole('link', { name: 'Date' }) + this.fileTitle = page.getByRole('link', { name: 'File' }) + this.successTitle = page.getByRole('link', { name: 'Success' }) + this.importDataButton = page.getByRole('link', { name: 'Import data ' }) + } + + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + + + async getDateTitle() { + + try { + return this.dateTitle; + } catch (error) { + console.error("Failed to get Data title:", error); + throw error; + } + } + + async getFileTitle() { + try { + return this.fileTitle; + } catch (error) { + console.error("Failed to get File title:", error); + throw error; + } + } + + async getSuccessTitle() { + try { + return this.successTitle; + } catch (error) { + console.error("Failed to get Success title:", error); + throw error; + } + } + + async getImportDataButton() { + try { + return this.importDataButton; + } catch (error) { + console.error("Failed to get Import Data button:", error); + throw error; + } + } + + async gotoImportDataPage() { + try { + (await this.getImportDataButton()).click(); + } catch (error) { + console.error("Failed to go to import data page:", error); + throw error; + } + } + + async orderTableByDate() { + (await this.getDateTitle()).click(); + } + + async fileIsAvailableInView(fileName: string): Promise { + + try { + console.log(`Checking if file ${fileName} was previously loaded`); + // Wait for the table to appear and contain at least one row + await this.page.waitForFunction(() => document.querySelectorAll('tr').length > 0, { timeout: 10000 }); + + const fileExists = await this.page.$$eval('td:nth-child(2)', (tds, fileName) => { + return tds.some(td => (td as HTMLElement).innerText === fileName); + }, fileName); + return fileExists; + + } catch (error) { + console.error("A problem while loading the list of files.", error); + throw error; + } + } + + async isFileSuccessfullyLoaded(fileName: string): Promise { + try { + const row = this.page.locator(`tr:has-text('${fileName}')`); + + if (!row) { + console.log(`No row found with file value: ${fileName}`); + return false; + } + const thirdCell = row.locator('td:nth-child(3)'); + if (thirdCell) { + const tickOrCross = await thirdCell.textContent(); + if (tickOrCross === '✔') { + return true; + } else if (tickOrCross === '✘') { + return false; + } + } + return false; + } catch (error) { + console.error('Error occurred:', error); + return false; + } + } + + async alertFileImportedSuccessfully(): Promise { + try { + await this.page.waitForSelector('.alert.alert-success.alert-dismissible'); + const element = await this.page.$('.alert.alert-success.alert-dismissible'); + + if (element !== null) { + const isVisible = await element.isVisible(); + if (isVisible) { + const text = await element.innerText(); + console.log(text); + expect(text).toBe(ALERT_FILE_IMPORTED_SUCCESSFULLY); + } + } + return false; + } catch (error) { + console.error("Failed to check for successful file import alert:", error); + throw error; + } + } + +} \ No newline at end of file diff --git a/src/page-objects/AD_ViewRolesPage.ts b/src/page-objects/AD_ViewRolesPage.ts new file mode 100644 index 0000000..7636fb4 --- /dev/null +++ b/src/page-objects/AD_ViewRolesPage.ts @@ -0,0 +1,37 @@ +import { Page, Locator } from "@playwright/test" + +export class ViewRolesPage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly nameColumnTitle: Locator + readonly descriptionColumnTitle: Locator + + constructor(page: Page) { + this.page = page; + this.primaryTitle = page.getByRole('heading', { name: 'Access control management' }); + this.secondaryTitle = page.getByRole('heading', { name: 'Manage roles' }); + this.nameColumnTitle = this.page.getByRole('button', { name: 'Name' }) + this.descriptionColumnTitle = this.page.getByRole('button', { name: 'Description' }) + } + + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + +} \ No newline at end of file diff --git a/src/page-objects/AD_ViewServicesPage.ts b/src/page-objects/AD_ViewServicesPage.ts new file mode 100644 index 0000000..ee5bfbd --- /dev/null +++ b/src/page-objects/AD_ViewServicesPage.ts @@ -0,0 +1,41 @@ +import { Page, Locator } from "@playwright/test" + +export class ViewServicesPage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly serviceColumnTitle: Locator + readonly descriptionColumnTitle: Locator + readonly roleColumnTitle: Locator + + constructor(page: Page) { + this.page = page; + this.primaryTitle = page.getByRole('heading', { name: 'Access control management' }); + this.secondaryTitle = page.getByRole('heading', { name: 'Manage services' }); + this.serviceColumnTitle = this.page.getByRole('button', { name: 'Service' }) + this.descriptionColumnTitle = this.page.getByRole('button', { name: 'Description' }) + this.roleColumnTitle = this.page.getByRole('button', { name: 'Role' }) + + } + + async getPrimaryTitle(){ + return this.primaryTitle.innerText(); + } + + async getSecondaryTitle(){ + return this.secondaryTitle.innerText(); + } + + async getServiceColumnTitle(){ + return this.serviceColumnTitle; + } + + async getDescriptionColumnTitle(){ + return this.descriptionColumnTitle; + } + + async getRoleColumnTitle(){ + return this.roleColumnTitle;; + } +} \ No newline at end of file diff --git a/src/page-objects/AD_ViewUsersPage.ts b/src/page-objects/AD_ViewUsersPage.ts new file mode 100644 index 0000000..bb50bc6 --- /dev/null +++ b/src/page-objects/AD_ViewUsersPage.ts @@ -0,0 +1,89 @@ +import { Page, Locator } from "@playwright/test" +import { User } from "../interfaces/User" + +export class ViewUsersPage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly lastNameColumnTitle: Locator + readonly firstNameColumnTitle: Locator + readonly emailColumnTitle: Locator + readonly membershipColumnTitle: Locator + readonly roleColumnTitle: Locator + + constructor(page: Page) { + this.page = page; + this.primaryTitle = page.getByRole('heading', { name: 'User management' }); + this.secondaryTitle = page.getByRole('heading', { name: ' View users' }); + this.lastNameColumnTitle = this.page.getByRole('button', { name: 'Last name' }) + this.firstNameColumnTitle = this.page.getByRole('button', { name: 'First name' }) + this.emailColumnTitle = this.page.getByRole('button', { name: 'Email' }) + this.membershipColumnTitle = this.page.getByRole('button', { name: 'Membership' }) + this.roleColumnTitle = this.page.getByRole('button', { name: 'Role' }) + } + + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title text:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title text:", error); + throw error; + } + } + + async userExists(email: string): Promise { + try { + console.log('Checking the user with email', email); + + // Wait for the table to appear and contain at least one row + await this.page.waitForFunction(() => document.querySelectorAll('tr').length > 0, { timeout: 10000 }); + + const userExists = await this.page.$$eval('td:nth-child(3)', (tds, email) => { + return tds.some(td => (td as HTMLElement).innerText === email); + }, email); + + return userExists; + } catch (error) { + console.error("The user does not exist!", error); + throw error; + } + } + + async gotoUserInformationPage(user: User) { + try { + let email = user.email; + const row = this.page.locator(`tr:has-text('${email}')`); + await row.locator('i.bi.bi-eye').click(); + } catch (error) { + console.error("Failed to view the details of the user:", error); + throw error; + } + + } + + + async deleteUser(email: string) { + + try { + this.page.getByRole('row', { name: email }).getByRole('link').click() + this.page.getByRole('link', { name: 'Delete' }).click(); + await this.page.waitForSelector('.modal-body', { timeout: 5000 }); + await this.page.click('a.btn.btn-danger'); //delete + //await page.click('button.btn.btn-secondary');//cancel + } catch (error) { + console.error("Failed to delete the user:", error); + throw error; + } + } + +} \ No newline at end of file diff --git a/src/page-objects/COMM_LoginPage.ts b/src/page-objects/COMM_LoginPage.ts new file mode 100644 index 0000000..427fbc9 --- /dev/null +++ b/src/page-objects/COMM_LoginPage.ts @@ -0,0 +1,58 @@ +import { type Page, type Locator } from '@playwright/test'; +import { ERROR_INCORRECT_EMAIL_PASSWORD } from '../constants/constants'; + +export class LogInPage { + readonly page: Page; + readonly emailInputLocator: Locator + readonly passwordInputLocator: Locator + readonly signInButtonLocator: Locator + + public constructor(page: Page) { + this.page = page; + this.emailInputLocator = page.getByPlaceholder('Email address') + this.passwordInputLocator = page.getByPlaceholder('Password') + this.signInButtonLocator = page.getByRole('button', { name: 'Log in' }) + } + async visit(site: string) { + try { + await this.page.goto(site); + } catch (error) { + console.error('Failed to navigate to the site:', error); + throw error; + } + } + async login(email: string, password: string) { + try { + await this.emailInputLocator.fill(email); + await this.passwordInputLocator.fill(password); + await this.signInButtonLocator.click(); + } catch (error) { + console.error('Login failed:', error); + throw new Error('Login failed'); + } + } + + async visitForgotPassword(site: string) { + try { + await this.page.goto(site); + } catch (error) { + console.error('Failed to navigate to the forgot password page:', error); + throw new error; + } + } + + async errorMessageIsValid(): Promise { + try { + const isVisible = await this.page.locator('.well.well-small.text-error').isVisible(); + if (!isVisible) { + return false; + } + const errorText = await this.page.locator('.well.well-small.text-error').textContent(); + return errorText === ERROR_INCORRECT_EMAIL_PASSWORD; + } catch (error) { + console.error('Failed to check error message:', error); + throw new error; + } + } +} + diff --git a/src/page-objects/US_LeftMenuUserPage.ts b/src/page-objects/US_LeftMenuUserPage.ts new file mode 100644 index 0000000..c36264e --- /dev/null +++ b/src/page-objects/US_LeftMenuUserPage.ts @@ -0,0 +1,59 @@ +import { Page, Locator } from "@playwright/test" + +export class LeftMenuUserPage { + readonly page: Page; + readonly dashboardLink: Locator + readonly myPersonalInfoLink: Locator + readonly myRolesLink: Locator + readonly myDataProtectionLink: Locator + readonly identitiesLink: Locator + readonly myCredentialsLink: Locator + readonly requestACredentialLink: Locator + readonly presentACredentialLink: Locator + + public constructor(page: Page) { + this.page = page; + this.dashboardLink = this.page.getByRole('link', { name: ' Dashboard' }) + this.myPersonalInfoLink = this.page.getByRole('link', { name: 'My personal information' }) + this.myRolesLink = this.page.getByRole('link', { name: 'My roles' }) + this.myDataProtectionLink = this.page.getByRole('link', { name: 'Data protection' }) + this.identitiesLink = this.page.getByRole('link', { name: 'Identities (DIDs)' }) + this.myCredentialsLink = this.page.getByRole('link', { name: 'My credentials' }) + this.requestACredentialLink = this.page.getByRole('link', { name: 'Request a credential' }) + this.presentACredentialLink = this.page.getByRole('link', { name: 'Present a credential' }) + } + + async getDashboardLink() { + return this.dashboardLink + } + + async getmyPersonalInfoLink() { + return this.myPersonalInfoLink + } + + async getMyRolesLink() { + return this.myRolesLink + } + + async getMyDataProtectionLink() { + return this.myDataProtectionLink + } + + async getIdentitiesLink() { + return this.identitiesLink + } + + async getMyCredentialsLink() { + return this.myCredentialsLink + } + + async getRequestACredentialLink() { + return this.requestACredentialLink + } + + async getPresentACredentialLink() { + return this.presentACredentialLink + } + + +} \ No newline at end of file diff --git a/src/page-objects/US_ViewCredentialPage.ts b/src/page-objects/US_ViewCredentialPage.ts new file mode 100644 index 0000000..e010da9 --- /dev/null +++ b/src/page-objects/US_ViewCredentialPage.ts @@ -0,0 +1,58 @@ +import { Page, Locator } from "@playwright/test" +import { initialize_with_undefined_values, setCredentialValues } from "../steps"; + +export class ViewCredentialPage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + + constructor(page: Page) { + this.page = page; + this.primaryTitle = page.getByRole('heading', { name: 'My wallet' }); + this.secondaryTitle = page.getByRole('heading', { name: ' Credential' }); + } + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + + async buildACredentialFromInputValues(credentialType: string) { + + const elementsWithLabel = await this.page.locator('.col-3 strong').all(); + const elementsWithValue = await this.page.locator('.col.bg-light.text-secondary').all(); + let credential = initialize_with_undefined_values(credentialType); + + let labels: string[] = []; + let values: string[] = []; + + for (let i = 0; i < elementsWithLabel.length; i++) { + let label = await elementsWithLabel[i].textContent() + if (label) label = label.trim().replace(/:$/, ''); + let value = await elementsWithValue[i].textContent() + if (value) value = value.trim(); + if (value != null && label != null && label != 'Id' && label != "Issuance date" && label != "Status") { + labels.push(label); + values.push(value); + } + } + + credential = setCredentialValues(labels, values, credentialType); + return credential; + + } + +} diff --git a/src/page-objects/US_ViewMyCredentialsPage.ts b/src/page-objects/US_ViewMyCredentialsPage.ts new file mode 100644 index 0000000..8b1fd81 --- /dev/null +++ b/src/page-objects/US_ViewMyCredentialsPage.ts @@ -0,0 +1,119 @@ +import { Page, Locator } from "@playwright/test" + +export class ViewMyCredentialsPage { + + readonly page: Page + readonly primaryTitle: Locator + readonly secondaryTitle: Locator + readonly typeTitle: Locator + readonly detailsTitle: Locator + readonly issuedOnTitle: Locator + readonly statusTitle: Locator + readonly viewCredentialButton: Locator + + + constructor(page: Page) { + this.page = page; + this.primaryTitle = page.getByRole('heading', { name: 'My wallet' }); + this.secondaryTitle = page.getByRole('heading', { name: 'Credential management' }); + this.typeTitle = page.getByRole('link', { name: 'Type' }) + this.detailsTitle = page.getByRole('link', { name: 'Details' }) + this.issuedOnTitle = page.getByRole('link', { name: 'Issued on' }) + this.statusTitle = page.getByRole('link', { name: 'Status' }) + this.viewCredentialButton = page.getByRole('link', { name: '' }) + + } + async getPrimaryTitle() { + try { + return await this.primaryTitle.innerText(); + } catch (error) { + console.error("Failed to get primary title:", error); + throw error; + } + } + + async getSecondaryTitle() { + try { + return await this.secondaryTitle.innerText(); + } catch (error) { + console.error("Failed to get secondary title:", error); + throw error; + } + } + + + async getTypeTitle() { + try { + return this.typeTitle; + } catch (error) { + console.error("Failed to get Type title:", error); + throw error; + } + } + + async getDetailsTitle() { + try { + return this.detailsTitle; + } catch (error) { + console.error("Failed to get Details title:", error); + throw error; + } + } + + async getIssuedOnTitle() { + try { + return this.issuedOnTitle; + } catch (error) { + console.error("Failed to get Issue On title:", error); + throw error; + } + } + + async getStatusTitle() { + try { + return this.statusTitle; + } catch (error) { + console.error("Failed to get Issue On title:", error); + throw error; + } + } + + + async orderTableByType() { + (await this.getTypeTitle()).click(); + } + + async orderTableByIssuedOn() { + (await this.getIssuedOnTitle()).click(); + } + + async gotoViewEnabledCredentialPage(type: string) { + + // Find the row that has the specified type and status 'Enabled' + const row = this.page.locator(`tr:has(td:nth-child(1):has-text("${type}"), td:nth-child(4):has-text("Enabled"))`); + + // Check if the row exists + if (await row.count() > 0) { + // Find the view credential button within the row and click it + await row.locator('i.bi.bi-eye').click(); + } else { + console.log(`No row found with type '${type}' and status 'Enabled'.`); + } + + } + + async enabledCredentialExistInMyCredentials(type: string): Promise { + + // Find the row that has the specified type and status 'Enabled' + const row = this.page.locator(`tr:has(td:nth-child(1):has-text("${type}"), td:nth-child(4):has-text("Enabled"))`); + + // Check if the row exists + if (await row.count() > 0) { + return true; + } else { + console.log(`No row found with type '${type}' and status 'Enabled'.`); + return false; + } + } + +} \ No newline at end of file diff --git a/src/steps.ts b/src/steps.ts new file mode 100644 index 0000000..8a9006c --- /dev/null +++ b/src/steps.ts @@ -0,0 +1,350 @@ +import { expect, Page } from '@playwright/test' + +import { appendRandomNumberToFilename, copyFile } from './utils' + +import { CREDENTIAL_TYPE_DATASTORE_UNDEFINED, CREDENTIAL_TYPES_DATA_STORE } from './data_stores/credentials_data_store' +import { FinancialVulnerabilityCredential_fields } from './interfaces/credential_interfaces' +import { CREDENTIAL_TYPES_FIELDS_LIST, CREDENTIAL_TYPES_REQUIRED_FIELDS_LIST } from './constants/credential_fields' +import { LogInPage } from './page-objects/COMM_LoginPage' +import { ADMIN_EMAIL, ADMIN_K, USER_K } from './constants/env_constants' +import { LeftMenuAdminPage } from './page-objects/AD_LeftMenuAdminPage' +import { LeftMenuUserPage } from './page-objects/US_LeftMenuUserPage' +import { TemplatesPage } from './page-objects/AD_TemplatesPage' +import { ViewUsersPage } from './page-objects/AD_ViewUsersPage' +import { ViewMyCredentialsPage } from './page-objects/US_ViewMyCredentialsPage' +import { UserPersonalInformationPage } from './page-objects/AD_UserPersonalInformationPage' +import { ImportTemplatePage } from './page-objects/AD_ImportTemplatePage' +import { ViewImportedDataPage } from './page-objects/AD_ViewImportedDataPage' +import { User } from './interfaces/User' + + +export async function loginAsAdmin(page: Page, url: string) { + try { + const loginPage = new LogInPage(page); + await loginPage.visit(url); + await loginPage.login(ADMIN_EMAIL, ADMIN_K); + + let currentTitle = await page.title(); + + if (currentTitle === 'Encryption Key – IdHub') { + //code to set Encription Key + page.getByPlaceholder('Key for encrypt the secrets').click(); + await page.getByPlaceholder('Key for encrypt the secrets').fill('1234'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page).toHaveTitle('Data protection – IdHub'); + currentTitle = await page.title(); + } + + if (currentTitle === 'Data protection – IdHub') { + // Code to accept terms and conditions + await page.click('#id_accept_privacy'); + await page.click('#id_accept_legal'); + await page.click('#id_accept_cookies'); + await page.click('a[type="button"]'); + } + await expect(page).toHaveTitle('Dashboard – IdHub'); + + } catch (error) { + console.error(`Failed to login as admin: `); + throw error; + } +} + +export async function loginAsUser(page: Page, userEmail: string, url: string) { + + try { + const loginPage = new LogInPage(page); + await loginPage.visit(url); + await loginPage.login(userEmail, USER_K); + + const currentTitle = await page.title(); + + if (currentTitle === 'Data Protection – IdHub') { + // Code to accept terms and conditions + await page.click('#id_accept_privacy'); + await page.click('#id_accept_legal'); + await page.click('#id_accept_cookies'); + await page.click('a[type="button"]'); + } + await expect(page).toHaveTitle('Dashboard – IdHub'); + } catch (error) { + console.error(`Failed to login as user: `); + throw error; + } +} + +export async function clickDashboardOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuAdminPage(page); + const dashboardLink = await leftMenuPage.getDashboardLink(); + await dashboardLink.click({ timeout: 60000 }); + return { dashboardLink } + } catch (error) { + console.error(`Failed to access Dashboard from left menu: `); + throw error; + } +} + +export async function clickUsersOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuAdminPage(page); + const usersLink = await leftMenuPage.getUsersLink(); + await usersLink.click() + return { usersLink } + } catch (error) { + console.error(`Failed to access Users option on left menu: `); + throw error; + } +} +export async function clickViewUsersOnLeftMenu(page: Page) { + try { + clickUsersOnLeftMenu(page); + const leftMenuPage = new LeftMenuAdminPage(page); + const viewUsersLink = await leftMenuPage.getViewUsersLink(); + await viewUsersLink.click(); + return { viewUsersLink } + } catch (error) { + console.error(`Failed to access View Users Option on left menu: `); + throw error; + } +} +export async function clickAddUserOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuAdminPage(page); + const addUserLink = await leftMenuPage.getAddUserLink(); + await addUserLink.click(); + return { addUserLink } + } catch (error) { + console.error(`Failed to access Add Users option on left menu: `); + throw error; + } +} + +export async function clickMyCredentialsOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuUserPage(page); + const myCredentialsLink = await leftMenuPage.getMyCredentialsLink(); + await myCredentialsLink.click(); + return { myCredentialsLink } + } catch (error) { + console.error(`Failed to access My Credential option on left menu: `); + throw error; + } +} + +export async function clickManageRolesOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuAdminPage(page); + const manageRolesLink = await leftMenuPage.getManageRolesLink(); + await manageRolesLink.click() + return { manageRolesLink } + } catch (error) { + console.error(`Failed to access Manage Roles option on left menu: `); + throw error; + } +} + +export async function clickManageServicesOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuAdminPage(page); + const manageRolesLink = await leftMenuPage.getManageServicesLink(); + await manageRolesLink.click() + return { manageRolesLink } + } catch (error) { + console.error(`Failed to access Manage Services option on left menu: `); + throw error; + } +} + +export async function clickTemplatesOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuAdminPage(page); + const templatesLink = await leftMenuPage.getTemplatesLink(); + await templatesLink.click() + return { templatesLink } + } catch (error) { + console.error(`Failed to access Templates option on left menu: `); + throw error; + } +} + +export async function clickDataOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuAdminPage(page); + const dataLink = await leftMenuPage.getDataLink(); + await dataLink.click() + return { dataLink } + } catch (error) { + console.error(`Failed access Data on left menu: `); + throw error; + } +} + +export async function clickCredentialsOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuAdminPage(page); + const credentialsLink = await leftMenuPage.getCredentialsLink(); + await credentialsLink.click() + return { credentialsLink } + } catch (error) { + console.error(`Failed to access Credentials On left Menu: `); + throw error; + } +} + +export async function clickViewCredentialsOnLeftMenu(page: Page) { + try { + const leftMenuPage = new LeftMenuAdminPage(page); + const viewCredentialsLink = await leftMenuPage.getViewCredentialsLink(); + await viewCredentialsLink.click() + return { viewCredentialsLink } + } catch (error) { + console.error(`Failed to access View Credential on left menu: `); + throw error; + } +} + +export async function clickViewSchemaOnLeftMenu(page: Page, templateName: string) { + try { + const templatesPage = new TemplatesPage(page) + //Access the specific template schema + const { templatesLink } = await clickTemplatesOnLeftMenu(page) + expect(await templatesPage.schemaIsAvailableInView(templateName)).toBeTruthy() + await templatesPage.gotoViewSchemaPage(templateName) + } catch (error) { + console.error(`Failed to access the Template option on left menu : `); + throw error; + } +} + + +export async function gotoBasicInfoPageOfTheUser(page: Page, user: User) { + + const viewUsersPage = new ViewUsersPage(page) + //Access the specific UPI + const { viewUsersLink } = await clickViewUsersOnLeftMenu(page) + try { + expect(await viewUsersPage.userExists(user.email)).toBeTruthy() + } catch (error) { + console.error('Failed validating that user exists', error); + } + await viewUsersPage.gotoUserInformationPage(user) +} + +export async function gotoViewEnabledCredential(page: Page, credentialType: string) { + const myCredentialsPage = new ViewMyCredentialsPage(page) + + //Access the specific enabled credential + const { myCredentialsLink } = await clickMyCredentialsOnLeftMenu(page); + try { + expect(await myCredentialsPage.enabledCredentialExistInMyCredentials(credentialType)).toBeTruthy(); + } catch (error) { + console.error('Failed accessing the enabled credential of type ${credentialType}', error); + } + await myCredentialsPage.gotoViewEnabledCredentialPage(credentialType); +} + +export async function checkIfTheInformationIsValid(page: Page, user: User) { + console.log(user.firstName, "membership: ", user.membershipType, "sd: ", user.startDate) + const userPersonalInformationPage = new UserPersonalInformationPage(page) + + try { + expect(await userPersonalInformationPage.isTheUserInfoValid(user)).toBeTruthy(); + } catch (error) { + console.error('Failed validating the user information:', error); + } +} + + +export function expectedCredentialSubjectForUser(email: string, credentialType: string): FinancialVulnerabilityCredential_fields | MembershipCard_fields | undefined { + try { + const testingDataForCredential = CREDENTIAL_TYPES_DATA_STORE[credentialType + '_data'] as T[]; + + if (!testingDataForCredential) { + console.error(`No testing data found for credential type: ${credentialType}`); + return undefined; + } + + console.log("testing Data: ", testingDataForCredential); + let credentialSubject = testingDataForCredential.find(user => user.email.toLowerCase() === email.toLowerCase()); + return credentialSubject; + } catch (error) { + console.error('Failed retriving the expected credential:', error); + } +} + +export async function importSchema(page: Page, jsonSchema: string) { + try { + const templatesPage = new TemplatesPage(page); + const importTemplatePage = new ImportTemplatePage(page) + await templatesPage.gotoAddTemplatePage(); + expect(importTemplatePage.schemaIsAvailableToImport(jsonSchema)).toBeTruthy(); + await importTemplatePage.gotoImportTemplate(jsonSchema); + return await templatesPage.schemaIsAvailableInView(jsonSchema); + } catch (error) { + console.error('Failed to import the schema:', error); + } +} + + +export async function checkFileName(page: Page, fileName: string): Promise { + const viewImportedDataPage = new ViewImportedDataPage(page); + let fileInTheList = await viewImportedDataPage.fileIsAvailableInView(fileName); + if (fileInTheList) { + let newName = appendRandomNumberToFilename(fileName); + //rename the file + copyFile(fileName, newName); + return newName; + } + return fileName; +} + +export async function loadIfJsonSchemaNotAvailable(page: Page, jsonSchema: string) { + try { + //Ensure that the json schema associated with the xls is available + const templatesPage = new TemplatesPage(page); + await clickTemplatesOnLeftMenu(page); + //check if the schema is available + let schemaAvailable = await templatesPage.schemaIsAvailableInView(jsonSchema); + if (!schemaAvailable) { + //import the schema + expect(await importSchema(page, jsonSchema)).toBeTruthy(); + } + } catch (error) { + console.error('Failed to load schema:', error); + } +} + +export function setCredentialValues(labels: string[], values: string[], credentialType: string): any { + // Get the interface type associated with the credentialType + + const listOfFields = CREDENTIAL_TYPES_FIELDS_LIST[credentialType]; + const listOfRequiredFields = CREDENTIAL_TYPES_REQUIRED_FIELDS_LIST[credentialType]; + + // Initialize an empty object to store the credential + let credential: any = {}; + try { + // Iterate over labels and values + for (let i = 0; i < labels.length; i++) { + let label = labels[i]; + let value = values[i]; + + // Check if the label exists in the interface type + label = label.charAt(0).toLowerCase() + label.slice(1); + if (listOfFields.includes(label)) { + credential[label] = value; + } else { + // If the label does not exist, add it as "unknown_data" + credential[label] = "unknown_data"; + } + } + // Return the credential object + return credential; + } catch (error) { + console.error('Failed to set credential values:', error); + } +} + +export function initialize_with_undefined_values(credentialType: string) { + return (CREDENTIAL_TYPE_DATASTORE_UNDEFINED[credentialType + '_data_undefined']) +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..1e3ed50 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,131 @@ +import { MEMBERSHIP_TYPES, PATH_FILES_TO_IMPORT } from "./constants/constants"; +import { unlink } from 'fs/promises'; + + +export function createUsersRandomList(num: number) { + let randomUsers: { firstName: string; lastName: string; email: string; membershipType: string; startDate: string; endDate: string }[] = [] + + for (let i = 0; i < num; i++) { + let randomName = generateRandomName() + let fn = randomName.split(' ')[0] + let ln = randomName.split(' ')[1] + let em = fn.toLowerCase() + '.' + ln.toLowerCase() + '@example.org'; + randomUsers.push({ + firstName: fn, + lastName: ln, + email: em, + membershipType: '2', + startDate: '2022-01-01', + endDate: '2030-01-01' + }); + } + let uniqueRandomUsers = randomUsers.filter((value, index, array) => + index === array.findIndex(item => + item.firstName === value.firstName && + item.lastName === value.lastName && + item.email === value.email + ) + ); + return uniqueRandomUsers +} + +function generateRandomName() { + var firstNames = ["Jan", "Pablo", "Ana", "Mari", "Jean", "Li", "Pau", "Glori", "Olga", "Sasha", "Diego", "Carla"]; + var lastNames = ["Garcia", "Sartre", "Cuevas", "Harrison", "Peña", "López", "Zapatero", "Dueñas", "Serrat", "Borgoñoz", "Suarez", "Craig"]; + + var randomFirstName = firstNames[Math.floor(Math.random() * firstNames.length)]; + var randomLastName = lastNames[Math.floor(Math.random() * lastNames.length)]; + + return `${randomFirstName} ${randomLastName}`; +} + +export function formatDate(dateStr: string) { + // Create a new Date object from the string + const date = new Date(dateStr); + + // Define the names of the months + const monthNames = ["Jan.", "Feb.", "March", "April", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."]; + + // Extract the day, month, and year + const day = date.getDate(); + const monthIndex = date.getMonth(); + const year = date.getFullYear(); + + // Return the formatted date + return `${monthNames[monthIndex]} ${day}, ${year}`; +} + +export async function copyFile(source: string, target: string) { + console.log("copyFile...."); + const fs = require('fs').promises; + const path = require('path'); + + let sourceFilePath = path.join(PATH_FILES_TO_IMPORT, source); + let targetFilePath = path.join(PATH_FILES_TO_IMPORT, target); + + console.log(`Current working directory: ${process.cwd()}`); + sourceFilePath = path.join(process.cwd(), sourceFilePath); + targetFilePath = path.join(process.cwd(), targetFilePath); + // + try { + await fs.access(sourceFilePath); + } catch (error) { + console.error(`Error accessing file: ${sourceFilePath}`); + throw error; + } + + try { + await fs.copyFile(sourceFilePath, targetFilePath); + + } catch (error) { + console.error(`Error renaming file: ${sourceFilePath} -> ${targetFilePath}`); + throw error; + } + + let existsNewFile: boolean; + try { + await fs.access(targetFilePath); + existsNewFile = true; + } catch (error) { + existsNewFile = false; + } + + if (!existsNewFile) { + throw new Error(`Failed to rename file: ${sourceFilePath} to ${targetFilePath}`); + } + +} + +export async function deleteFile(fileName: string): Promise { + const path = require('path'); + + try { + let filePath = path.join(PATH_FILES_TO_IMPORT, fileName); + filePath = path.join(process.cwd(), filePath); + await unlink(filePath); + console.log(`File deleted: ${filePath}`); + } catch (error) { + console.error(`Error deleting file: ${fileName}. Error: ${error}`); + } +} + +export function appendRandomNumberToFilename(filename: string) { + // Generate a random number between 100 and 999 + const randomNumber = Math.floor(Math.random() * (999 - 100 + 1)) + 100; + + // Extract the extension from the filename + const extensionIndex = filename.lastIndexOf('.'); + const extension = extensionIndex !== -1 ? filename.slice(extensionIndex) : ''; + + // Remove the extension from the original filename + const basename = extensionIndex !== -1 ? filename.slice(0, extensionIndex) : filename; + + // Append the random number and the extension to form the new filename + return `${basename}${randomNumber}${extension}`; +} + + +export function getMembershipTypeById(id: string) { + const item = MEMBERSHIP_TYPES.find(item => item.id === id); + return item ? item.type : null; +} \ No newline at end of file diff --git a/tests/.DS_Store b/tests/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 { + test('Successful login as admin', async ({ page }) => { + await loginAsAdmin(page, URL_IDHUB); + await expect.soft(page).toHaveTitle('Dashboard – IdHub'); + }) + test('Unsuccessful login as admin', async ({ page }) => { + const loginPage = new LogInPage(page) + await loginPage.visit(URL_IDHUB); + await loginPage.login(ADMIN_EMAIL, KO_ADMIN_K) + await expect.soft(loginPage.errorMessageIsValid()).toBeTruthy(); + }) + test('Navigate to Forgot password page from login page', async ({ page }) => { + const loginPage = new LogInPage(page) + await loginPage.visitForgotPassword(URL_PASS_RESET) + await expect.soft(page).toHaveTitle('Password reset – IdHub'); + }) + +}) + +test.describe('User login functionality', () => { + test('Successful login as user', async ({ page }) => { + await loginAsUser(page, USER1_EMAIL, URL_IDHUB); + await expect.soft(page).toHaveTitle('Dashboard – IdHub'); + //TODO: check email at the right corner + }) + test('Unsuccessful login as user', async ({ page }) => { + const loginPage = new LogInPage(page) + await loginPage.visit(URL_IDHUB); + await loginPage.login(USER1_EMAIL, KO_USER_K) + await expect.soft(loginPage.errorMessageIsValid()).toBeTruthy(); + }) + +}) \ No newline at end of file diff --git a/tests/01-AD-leftmenuNavigation.spec.ts b/tests/01-AD-leftmenuNavigation.spec.ts new file mode 100644 index 0000000..b668b6e --- /dev/null +++ b/tests/01-AD-leftmenuNavigation.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from '@playwright/test' +import { LeftMenuAdminPage } from '../src/page-objects/AD_LeftMenuAdminPage' +import { ViewUsersPage } from '../src/page-objects/AD_ViewUsersPage'; +import { BasicUserInfoSectionPage } from '../src/page-objects/AD_BasicUserInfoSectionInPage'; +import { ViewRolesPage } from '../src/page-objects/AD_ViewRolesPage'; +import { ViewServicesPage } from '../src/page-objects/AD_ViewServicesPage'; +import { clickAddUserOnLeftMenu, clickDashboardOnLeftMenu, clickManageRolesOnLeftMenu, clickManageServicesOnLeftMenu, clickViewUsersOnLeftMenu, loginAsAdmin } from '../src/steps'; +import { URL_IDHUB } from '../src/constants/env_constants'; + +/** + * Check the navigation options on the Left Menu + */ +test.describe('Leftside Menu navigation test', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page, URL_IDHUB); + }) + + test('LEFTMENU -> Dashboard', async ({ page }) => { + await clickDashboardOnLeftMenu(page) + await expect(page).toHaveTitle('Dashboard – IdHub'); + }) + + test('LEFTMENU -> Users', async ({ page }) => { + + await clickViewUsersOnLeftMenu(page); + await expect(page).toHaveTitle('User management – IdHub'); + //Check titles + const viewUsersPage = new ViewUsersPage(page); + expect(await viewUsersPage.getPrimaryTitle()).toEqual("User management"); + expect(await viewUsersPage.getSecondaryTitle()).toEqual(" View users"); + + //Navigate to "Add user" + const basicInfoPage = new BasicUserInfoSectionPage(page); + await clickAddUserOnLeftMenu(page); + + //Check titles + expect(await basicInfoPage.getPrimaryTitleText()).toEqual("User management"); + expect(await basicInfoPage.getSecondaryTitleText()).toEqual(" Add user"); + }) + + test('LEFTMENU -> Roles and Services', async ({ page }) => { + + //Navigate to "Roles" + const leftMenu = new LeftMenuAdminPage(page); + (await leftMenu.getRolesLink()).click(); + + //Navigate to "Manage roles" + await clickManageRolesOnLeftMenu(page); + await expect(page).toHaveTitle('Access control management – IdHub'); + + //Check titles + const viewRolesPage = new ViewRolesPage(page); + expect(await viewRolesPage.getPrimaryTitle()).toEqual("Access control management"); + expect(await viewRolesPage.getSecondaryTitle()).toEqual("Manage roles"); + + //Navigate to "Manage services" + await clickManageServicesOnLeftMenu(page); + await expect(page).toHaveTitle('Access control management – IdHub'); + + //Check titles + const viewServicesPage = new ViewServicesPage(page); + expect(await viewServicesPage.getPrimaryTitle()).toEqual("Access control management"); + expect(await viewServicesPage.getSecondaryTitle()).toEqual("Manage services"); + + }) + + //TODO: credentials, templates.... + + test.skip('LEFTMENU -> Credentials', async ({ page }) => { + }) + + test.skip('LEFTMENU -> Templates', async ({ page }) => { + }) + + test.skip('LEFTMENU -> Data', async ({ page }) => { + }) + +}) \ No newline at end of file diff --git a/tests/02-AD-manageUsers.spec.ts b/tests/02-AD-manageUsers.spec.ts new file mode 100644 index 0000000..54599cf --- /dev/null +++ b/tests/02-AD-manageUsers.spec.ts @@ -0,0 +1,325 @@ +import { test, expect, Page } from '@playwright/test' +import { BasicUserInfoSectionPage } from '../src/page-objects/AD_BasicUserInfoSectionInPage' +import { LeftMenuAdminPage } from '../src/page-objects/AD_LeftMenuAdminPage' +import { ViewUsersPage } from '../src/page-objects/AD_ViewUsersPage' +import { AddMembershipPage } from '../src/page-objects/AD_AddMembershipPage' +import { UserPersonalInformationPage } from '../src/page-objects/AD_UserPersonalInformationPage' +import { MembershipSectionPage } from '../src/page-objects/AD_MembershipSectionPage' +import { testingUsers, users } from '../src/data_stores/usersDataInitialization' +import { createUsersRandomList } from '../src/utils' +import { URL_IDHUB } from '../src/constants/env_constants' +import { checkIfTheInformationIsValid, clickAddUserOnLeftMenu, clickViewUsersOnLeftMenu, gotoBasicInfoPageOfTheUser, loginAsAdmin } from '../src/steps' + +/** + * Testing Administrator's interface - USERS section functionality + */ + +test.describe('USER Section Tests', () => { + + test.beforeEach(async ({ page }) => { //executed at the beginning of each test + const leftMenu = new LeftMenuAdminPage(page); + await loginAsAdmin(page, URL_IDHUB); + (await leftMenu.getUsersLink()).click() + }) + /** + * UPDATE the basic data for default testing users user1..user5 + */ + + test('USERS -> Modify personal data for testing users (user<#>.example.org) with meaningful basic data', async ({ page }) => { + // Initialize pages + const basicInfoPage = new BasicUserInfoSectionPage(page); + const userPersonalInformationPage = new UserPersonalInformationPage(page); + + for (let user of testingUsers) { + console.log("Modifying user: ", user) + await gotoBasicInfoPageOfTheUser(page, user); + await userPersonalInformationPage.selectModifyUser(); + await basicInfoPage.updateUserBasicInfo(user.firstName, user.lastName, user.email, false); + console.log("Updated basic data for user: ", user.email) + } + + }); + + /** + * Add membership data for default testing users user1..user5 + */ + + test('USERS -> Modify membership data for testing users (user<#>.example.org)', async ({ page }) => { + // Initialize pages + const userPersonalInformationPage = new UserPersonalInformationPage(page); + const membershipSectionPage = new MembershipSectionPage(page); + const addMembershipPage = new AddMembershipPage(page); + + for (let user of testingUsers) { + console.log("Modifying membership for user: ", user) + // Update the membership + await gotoBasicInfoPageOfTheUser(page, user); + await userPersonalInformationPage.selectModifyUser(); + await membershipSectionPage.selectAddMembershipButton(); + + await addMembershipPage.addNewUserMembership('2', '2022-01-12', '2026-01-12') + console.log("Create a new membership for user: ", user.email) + + // Validate the updated user data + await gotoBasicInfoPageOfTheUser(page, user); + expect(await membershipSectionPage.membershipExist(page, '2')).toBeTruthy() + } + + }); + + /** + * Assign user5 as Idhub administrator + */ + test('USERS -> Modify personal data -> Assign user5 as Idhub administrator ', async ({ page }) => { + // Initialize pages + const basicInfoPage = new BasicUserInfoSectionPage(page); + const userPersonalInformationPage = new UserPersonalInformationPage(page); + + console.log("Modifying user: ", testingUsers[4]); + let user = testingUsers[4]; + await gotoBasicInfoPageOfTheUser(page, user); + await userPersonalInformationPage.selectModifyUser(); + await basicInfoPage.updateUserBasicInfo(user.firstName, user.lastName, user.email, true); + console.log("Updated basic data for user: ", user.email) + + }); + + /** + * Create a list of random users + * ADD the USER (basic info) to the idhub + * Validate infomation after addition + */ + + test.skip('USERS -> Add user: Add random users with basic data', async ({ page }) => { + + const randomUsers = createUsersRandomList(6) + for (let user of randomUsers) { + console.log(user) + } + + // Initialize pages + const basicInfoPage = new BasicUserInfoSectionPage(page); + const membershipPage = new AddMembershipPage(page); + + // Navigate to the Add User page + const { addUserLink } = await clickAddUserOnLeftMenu(page); + + // Add the list of users + for (let user of randomUsers) { + await basicInfoPage.addUserBasicInfo(user.firstName, user.lastName, user.email, false); + expect(await membershipPage.alertUserCreationMessageIsValid()).toBeTruthy(); + await membershipPage.addUserMembership(user.membershipType, user.startDate, user.endDate); + await addUserLink.click(); + } + + // Check if the users are visible in 'View users' panel + const viewUsersPage = new ViewUsersPage(page); + const { viewUsersLink } = await clickViewUsersOnLeftMenu(page); + for (let user of randomUsers) { + expect(await viewUsersPage.userExists(user.email)).toBeTruthy(); + await viewUsersLink.click(); + } + + }); + + /** + * ADD a user (basic information) and validate it after addition + * UPDATE their basic information and validate it after modification + * DELETE the USER added before + */ + test('USERS -> Update basic user information for a user', async ({ page }) => { + + // Initialize pages + const basicInfoPage = new BasicUserInfoSectionPage(page); + const userPersonalInformationPage = new UserPersonalInformationPage(page); + const membershipPage = new AddMembershipPage(page); + + // Define user data + let user = { + firstName: 'James', + lastName: 'Swift', + email: 'james.swift@example.org', + membershipType: '1', + startDate: '2023-01-01', + endDate: '2024-01-01' + }; + + // Generate updated user data + const fn = user.firstName.concat(' Junior'); + const ln = user.lastName.concat(' Anderson'); + const em = fn.toLowerCase().replace(/\s/g, '') + '.' + ln.toLowerCase().replace(/\s/g, '') + '@example.org'; + + let updateData = { + firstName: fn, + lastName: ln, + email: em, + membershipType: user.membershipType, + startDate: user.startDate, + endDate: user.endDate + }; + + // Navigate to the Add User page + const { addUserLink } = await clickAddUserOnLeftMenu(page); + + // Create a new user + await basicInfoPage.addUserBasicInfo(user.firstName, user.lastName, user.email, false); + expect(await membershipPage.alertUserCreationMessageIsValid()).toBeTruthy(); + await membershipPage.addUserMembership(user.membershipType, user.startDate, user.endDate); + await addUserLink.click(); + console.log("Created user: ", user.email) + + // Validate the newly created user + await gotoBasicInfoPageOfTheUser(page, user); + await checkIfTheInformationIsValid(page, user); + + // Update the user data + await gotoBasicInfoPageOfTheUser(page, user); + await userPersonalInformationPage.selectModifyUser(); + await basicInfoPage.updateUserBasicInfo(updateData.firstName, updateData.lastName, updateData.email, false); + + console.log("Updated basic data for user: ", user.email) + + // Validate the updated user data + await gotoBasicInfoPageOfTheUser(page, updateData); + await checkIfTheInformationIsValid(page, updateData); + + //Delete the user + await gotoBasicInfoPageOfTheUser(page, updateData); + await userPersonalInformationPage.deleteUser(page); + console.log("Deleted user: ", user.email) + + + }); + + /** + * ADD a user (basic info) and membership + * UPDATE the MEMBERSHIP information and validate after modification + * ADD a second MEMBERSHIP and validate + * DELETE the first added MEMBERSHIP and validate + * Delete the user + */ + test('USERS -> Add, modify, delete a membershipType', async ({ page }) => { + // Initialize pages + const basicInfoPage = new BasicUserInfoSectionPage(page); + const userPersonalInformationPage = new UserPersonalInformationPage(page); + const addMembershipPage = new AddMembershipPage(page); + const membershipSectionPage = new MembershipSectionPage(page); + + // Navigate to the Add User page + const { addUserLink } = await clickAddUserOnLeftMenu(page); + + // Define user data + let user = { + firstName: 'Caroline', + lastName: 'Harari', + email: 'caroline.harari' + '@example.org', + membershipType: '1', + startDate: '2023-01-01', + endDate: '2024-01-01' + }; + + + let updateData = { + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + membershipType: '2', + startDate: '2024-01-04', + endDate: '2025-04-04' + }; + + // Create a new user + await basicInfoPage.addUserBasicInfo(user.firstName, user.lastName, user.email, false); + expect(await addMembershipPage.alertUserCreationMessageIsValid()).toBeTruthy(); + await addMembershipPage.addUserMembership(user.membershipType, user.startDate, user.endDate); + await addUserLink.click(); + console.log("Created user: ", user.email) + + // Validate the newly created user + await gotoBasicInfoPageOfTheUser(page, user); + await checkIfTheInformationIsValid(page, user); + + // Update the membership + await gotoBasicInfoPageOfTheUser(page, user); + await userPersonalInformationPage.selectModifyUser(); + await membershipSectionPage.gotoModifyMembershipPage(page, user.membershipType); + await addMembershipPage.updateExistingUserMembership(page, user.membershipType, updateData.membershipType, updateData.startDate, updateData.endDate) + + console.log("Updated membership for user: ", user.email) + + // Validate the updated user data + await gotoBasicInfoPageOfTheUser(page, updateData); + await checkIfTheInformationIsValid(page, updateData); + + //Add a second membership + await gotoBasicInfoPageOfTheUser(page, user); + await userPersonalInformationPage.selectModifyUser(); + await membershipSectionPage.selectAddMembershipButton(); + await addMembershipPage.addNewUserMembership('3', '2022-01-12', '2025-01-12'); + console.log("Updated with second membership for user: ", user.email); + + // Validate the updated user data + await gotoBasicInfoPageOfTheUser(page, updateData); + expect(await membershipSectionPage.membershipExist(page, '3')).toBeTruthy() + + //Delete the first membership + await gotoBasicInfoPageOfTheUser(page, user); + await userPersonalInformationPage.selectModifyUser(); + await membershipSectionPage.gotoDeleteSpecificMembership(page, updateData.membershipType) + console.log("Deleted membership for user: ", user.email) + + // Validate the updated user data + await gotoBasicInfoPageOfTheUser(page, updateData); + expect(await membershipSectionPage.membershipExist(page, updateData.membershipType)).toBeFalsy() + + //Delete the user + await gotoBasicInfoPageOfTheUser(page, updateData); + await userPersonalInformationPage.deleteUser(page); + console.log("Deleted user: ", user.email) + }); + /** + * Create a pre-defined group of users + * Add the users (basic information) to the idhub + * Validate the users information after addition + */ + test('USERS -> Add, Modify and delete users: Add a group of users, with basic info and membership, then delete', async ({ page }) => { + // Initialize pages + const basicInfoPage = new BasicUserInfoSectionPage(page); + const membershipPage = new AddMembershipPage(page); + const userPersonalInformationPage = new UserPersonalInformationPage(page); + for (let user of users) { + console.log(user) + } + // Navigate to the Add User page + const { addUserLink } = await clickAddUserOnLeftMenu(page); + + // Add the list of users + for (let user of users) { + await basicInfoPage.addUserBasicInfo(user.firstName, user.lastName, user.email, false); + expect(await membershipPage.alertUserCreationMessageIsValid()).toBeTruthy(); + await membershipPage.addUserMembership(user.membershipType, user.startDate, user.endDate); + await addUserLink.click(); + } + + // Check if the users are visible in 'View users' + const viewUsersPage = new ViewUsersPage(page); + const { viewUsersLink } = await clickViewUsersOnLeftMenu(page); + for (let user of users) { + expect(await viewUsersPage.userExists(user.email)).toBeTruthy(); + await viewUsersLink.click(); + } + + // Delete all the users in the list + await clickViewUsersOnLeftMenu(page); + for (let user of users) { + await gotoBasicInfoPageOfTheUser(page, user); + await userPersonalInformationPage.deleteUser(page); + + } + await clickViewUsersOnLeftMenu(page); + for (let user of users) { + expect(await viewUsersPage.userExists(user.email)).toBeFalsy(); + } + }); + +}) diff --git a/tests/03-COMM-templatesAndDataFiles.spec.ts b/tests/03-COMM-templatesAndDataFiles.spec.ts new file mode 100644 index 0000000..650b884 --- /dev/null +++ b/tests/03-COMM-templatesAndDataFiles.spec.ts @@ -0,0 +1,310 @@ +import { test, expect} from '@playwright/test' +import { TemplatesPage } from '../src/page-objects/AD_TemplatesPage' +import { ViewImportedDataPage } from '../src/page-objects/AD_ViewImportedDataPage' +import { ImportDataPage } from '../src/page-objects/AD_ImportDataPage' +import { checkFileName, clickDataOnLeftMenu, clickTemplatesOnLeftMenu, loadIfJsonSchemaNotAvailable, loginAsAdmin, loginAsUser, gotoViewEnabledCredential, expectedCredentialSubjectForUser } from '../src/steps'; +import { ViewCredentialPage } from '../src/page-objects/US_ViewCredentialPage.js' +import { fail } from 'assert' +import { URL_IDHUB, USER1_EMAIL } from '../src/constants/env_constants'; +import { ALERT_FILE_TO_IMPORT_IS_EMPTY, ALERT_FILE_TO_IMPORT_WITHOUT_REQUIRED_COLUMS, ALERT_FILE_TO_IMPORT_WITH_ALIEN_COLUMS, FILE_TO_IMPORT_FVC, FILE_TO_IMPORT_FVC_EMPTY, FILE_TO_IMPORT_FVC_WITHOUT_REQUIRED_COLUMNS, FILE_TO_IMPORT_FVC_WITH_ALIEN_COLUMNS, JSON_SCHEMA_FVC, JSON_SCHEMA_ID_FVC, PATH_FILES_TO_IMPORT, SCHEMA_FVC, SCHEMA_TYPE_FVC } from '../src/constants/constants'; +import { deleteFile } from '../src/utils'; + +/** +* Checking template section: view the lists of templates, import schema, delete schema, view json +*/ +test.describe('TEMPLATES Section Tests ', () => { + + test.beforeEach(async ({ page }) => { //este se ejecutará antes de cada test + await loginAsAdmin(page, URL_IDHUB); + }) + + /** + * For every row in the templates view, + * extract the name of the template schema (second cell) and compare it against the + * las element of the $id in the corresponding json file. + */ + + test('TEMPLATES -> View credential templates -> compare each template name against last element of $id in JSON', async ({ page }) => { + // Navigate to the page with the table + const templatesPage = new TemplatesPage(page); + await clickTemplatesOnLeftMenu(page); + expect(await templatesPage.checkSchemaNamesAgainstCorrespondingJSON(page)).toBeTruthy(); + }) + + /** + * Check a specific template in the list of templates + * If the template schema is not available, the schema is imported + * Once the template schema is in the list, verify the element id in the json file + */ + test('TEMPLATES -> View schema/import for Financial Vulnerability Credential', async ({ page }) => { + const templatesPage = new TemplatesPage(page); + //Load the schema if it is not loaded + await loadIfJsonSchemaNotAvailable(page, JSON_SCHEMA_FVC); + + //validate $id in the json template + await templatesPage.gotoViewSchemaPage(JSON_SCHEMA_FVC); + const jsonContent = await page.evaluate(() => JSON.parse(document.body.innerText)); + // Check if the JSON elements exist + expect(jsonContent["$id"]).toBe(JSON_SCHEMA_ID_FVC); + }) + + /** + * Check a specific template in the list of templates + * If the template schema is not available, the schema is imported and verify the operation + * Try to delete the schema, but cancel the operation + * Try to delete the schema, confirm the operation and verify the operation + */ + test('TEMPLATES -> Delete schema', async ({ page }) => { + //Access the specific template schema + const templatesPage = new TemplatesPage(page); + + //check if the schema is imported + await loadIfJsonSchemaNotAvailable(page, JSON_SCHEMA_FVC); + + /*Try to delete the schema and then cancel in the modal*/ + await templatesPage.gotoDeleteAndCancelInModal(JSON_SCHEMA_FVC); + /*Verify the schema was imported*/ + expect(await templatesPage.schemaIsAvailableInView(JSON_SCHEMA_FVC)).toBeTruthy(); + + /*Try to delete the schema and thenn confirm the operation*/ + await templatesPage.gotoDeleteAndConfirmInModal(JSON_SCHEMA_FVC); + /*Verify the schema was deleted*/ + expect(await templatesPage.schemaIsAvailableInView(JSON_SCHEMA_FVC)).toBeFalsy(); + + }) +}) + +/** +* Checking data section: view the lists of files imported, import data, delete... +*/ + +test.describe('DATA Section Tests', () => { + + test.beforeEach(async ({ page }) => { //este se ejecutará antes de cada test + await loginAsAdmin(page, URL_IDHUB); + }) + + + /** + * Load of an excel file - Happy Path: + * Check if file was loaded before. If yes, copy the original file (financial-vulnerability-credential.xlxs) to a new valid with random name + * If the template schema (associated with the type of credential to be enabled) is not available, import it and verify the operation + * Load the associated data file (xls) and check that appears in the list of imported data files + * Expected behavior: the file is loaded, the message:"The file was imported successfully!" is displayed, + * and the file appears in the imported files view. + */ + + test('DATA -> Import data file - Happy path - Financial Vulnerability Credential ', async ({ page }) => { + + const viewImportedDataPage = new ViewImportedDataPage(page); + const importDataPage = new ImportDataPage(page); + + await clickDataOnLeftMenu(page); + + // If a file with a FILE_TO_IMPORT was previouly loaded, rename it + let fileName = FILE_TO_IMPORT_FVC; + fileName = await checkFileName(page, fileName); + + // Check if the json schema associated with the file is loaded to be used + // If not, load it in templates + await loadIfJsonSchemaNotAvailable(page, JSON_SCHEMA_FVC); + + //Go to the Data option on leftmenu + await clickDataOnLeftMenu(page); + + //Select the import button and go to the import data page + await viewImportedDataPage.gotoImportDataPage(); + + //Import excel file to enable a 'Financial vulnerability credential'for testing users + console.log("File to import: ", fileName, " with schema: ", SCHEMA_FVC); + await importDataPage.importFile(SCHEMA_FVC, PATH_FILES_TO_IMPORT + fileName, "Default"); + let actual = await viewImportedDataPage.alertFileImportedSuccessfully(); + + //Rename the default excel file with the original name FILE_TO_IMPORT in the directoy PATH_FILES_TO_IMPORT + if (fileName != FILE_TO_IMPORT_FVC) { + await deleteFile(fileName); + } + if (actual) { + //Check if the file is in the list of imported files sucessfully + await clickDataOnLeftMenu(page); + await page.getByRole('link', { name: 'Date' }).click(); + await page.getByRole('link', { name: 'Date' }).click(); + + expect(await viewImportedDataPage.fileIsAvailableInView(fileName)).toBeTruthy(); + expect(await viewImportedDataPage.isFileSuccessfullyLoaded(fileName)).toBeTruthy(); + } else { + console.log("Unexpected result loading the file. Test failed.") + fail(); + } + + }); + + /** + * Load of an excel file - Sad Path: + * Check if file was loaded before. If yes, copy the original to the new one with random name. + * If the template schema (associated with the type of credential to be enabled) is not available, import it and verify the operation + * Try to load a well-formatted excel file but without data. + * Expected behavior: The error message: "The file you try to import is empty" + */ + + test('DATA -> Import data file - Sad path (file well formatted but empty) - Financial Vulnerability Credential ', async ({ page }) => { + + const viewImportedDataPage = new ViewImportedDataPage(page); + const importDataPage = new ImportDataPage(page); + + await clickDataOnLeftMenu(page); + + // If a file with a FILE_TO_IMPORT was previously loaded, rename it + let fileName = await checkFileName(page, FILE_TO_IMPORT_FVC_EMPTY); + + // Check if the json schema associated with the file is loaded to be used + // If not, load it in templates + await loadIfJsonSchemaNotAvailable(page, JSON_SCHEMA_FVC); + + //Go to the Data option on leftmenu + await clickDataOnLeftMenu(page); + + //Select the import button and go to the import data page + await viewImportedDataPage.gotoImportDataPage(); + + //Import excel file to enable a 'Financial vulnerability credential'. The file has no data. + console.log("File to import: ", fileName, " with schema: ", SCHEMA_FVC); + await importDataPage.importFile(SCHEMA_FVC, PATH_FILES_TO_IMPORT + fileName, "Default"); + let actual = await importDataPage.alertFileImportedUnsuccessfully(ALERT_FILE_TO_IMPORT_IS_EMPTY); + + //Rename the default excel file with the original name FILE_TO_IMPORT in the directoy PATH_FILES_TO_IMPORT + if (fileName != FILE_TO_IMPORT_FVC_EMPTY) { + await deleteFile(fileName); + } + if (actual) { + //Check if the file is in the list of imported files as failed + await clickDataOnLeftMenu(page); + await page.getByRole('link', { name: 'Date' }).click(); + await page.getByRole('link', { name: 'Date' }).click(); + + expect(await viewImportedDataPage.fileIsAvailableInView(fileName)).toBeTruthy(); + expect(await viewImportedDataPage.isFileSuccessfullyLoaded(fileName)).toBeFalsy(); + } else { + console.log("Unexpected result loading an empty file. Test failed."); + fail(); + } + }); + + test('DATA -> Import data file - Sad path (file bad formatted, without required columns) - Financial Vulnerability Credential ', async ({ page }) => { + + const viewImportedDataPage = new ViewImportedDataPage(page); + const importDataPage = new ImportDataPage(page); + + await clickDataOnLeftMenu(page); + + // If a file with a FILE_TO_IMPORT was previously loaded, rename it + let fileName = await checkFileName(page, FILE_TO_IMPORT_FVC_WITHOUT_REQUIRED_COLUMNS); + + // Check if the json schema associated with the file is loaded to be used + // If not, load it in templates + await loadIfJsonSchemaNotAvailable(page, JSON_SCHEMA_FVC); + + //Go to the Data option on leftmenu + await clickDataOnLeftMenu(page); + + //Select the import button and go to the import data page + await viewImportedDataPage.gotoImportDataPage(); + + //Import excel file to enable a 'Financial vulnerability credential'. The file has no data. + console.log("File to import: ", fileName, " with schema: ", SCHEMA_FVC); + await importDataPage.importFile(SCHEMA_FVC, PATH_FILES_TO_IMPORT + fileName, "Default"); + let actual = await importDataPage.alertFileImportedUnsuccessfully(ALERT_FILE_TO_IMPORT_WITHOUT_REQUIRED_COLUMS); + + //Rename the default excel file with the original name FILE_TO_IMPORT in the directoy PATH_FILES_TO_IMPORT + if (fileName != FILE_TO_IMPORT_FVC_WITHOUT_REQUIRED_COLUMNS) { + await deleteFile(fileName); + } + if (actual) { + //Check if the file is in the list of imported files as failed + await clickDataOnLeftMenu(page); + await page.getByRole('link', { name: 'Date' }).click(); + await page.getByRole('link', { name: 'Date' }).click(); + + expect(await viewImportedDataPage.fileIsAvailableInView(fileName)).toBeTruthy(); + expect(await viewImportedDataPage.isFileSuccessfullyLoaded(fileName)).toBeFalsy(); + } else { + console.log("Unexpected result loading an empty file. Test failed."); + fail(); + } + + }); + + test('DATA -> Import data file - Sad path (file bad formatted, with alien columns) - Financial Vulnerability Credential ', async ({ page }) => { + + const viewImportedDataPage = new ViewImportedDataPage(page); + const importDataPage = new ImportDataPage(page); + + await clickDataOnLeftMenu(page); + + // If a file with a FILE_TO_IMPORT was previously loaded, rename it + let fileName = await checkFileName(page, FILE_TO_IMPORT_FVC_WITH_ALIEN_COLUMNS); + + // Check if the json schema associated with the file is loaded to be used + // If not, load it in templates + await loadIfJsonSchemaNotAvailable(page, JSON_SCHEMA_FVC); + + //Go to the Data option on leftmenu + await clickDataOnLeftMenu(page); + + //Select the import button and go to the import data page + await viewImportedDataPage.gotoImportDataPage(); + + //Import excel file to enable a 'Financial vulnerability credential'. The file has no data. + console.log("File to import: ", fileName, " with schema: ", SCHEMA_FVC); + await importDataPage.importFile(SCHEMA_FVC, PATH_FILES_TO_IMPORT + fileName, "Default"); + let actual = await importDataPage.alertFileImportedUnsuccessfully(ALERT_FILE_TO_IMPORT_WITH_ALIEN_COLUMS); + + //Rename the default excel file with the original name FILE_TO_IMPORT in the directoy PATH_FILES_TO_IMPORT + if (fileName != FILE_TO_IMPORT_FVC_WITH_ALIEN_COLUMNS) { + await deleteFile(fileName); + } + if (actual) { + //Check if the file is in the list of imported files as failed + await clickDataOnLeftMenu(page); + await page.getByRole('link', { name: 'Date' }).click(); + await page.getByRole('link', { name: 'Date' }).click(); + + expect(await viewImportedDataPage.fileIsAvailableInView(fileName)).toBeTruthy(); + expect(await viewImportedDataPage.isFileSuccessfullyLoaded(fileName)).toBeFalsy(); + } else { + console.log("Unexpected result loading an empty file. Test failed."); + fail(); + } + + }); + +}) //end describe + +test.describe('USER Credentials Section Tests', () => { + + test.beforeEach(async ({ page }) => { //este se ejecutará antes de cada test + await loginAsUser(page, USER1_EMAIL, URL_IDHUB); + }) + + /** + * Check if the user1 can visualize the credentials that has been enabled in "My Credentials" + * Check the fields displayed when user click "View" Credential + */ + + test('USER Credentials -> My Credentials -> View enabled Financial Vulnerability Credential', async ({ page }) => { + // View the Financial Vulnerabilty Credential in status 'Enabled' for the user + const credentialStatus = "Enabled" + await gotoViewEnabledCredential(page, SCHEMA_TYPE_FVC); + const enabledCredentialView = new ViewCredentialPage(page); + //Check that required fields exist and have a valid value in the current enabled credential + + //Get the credential subject values of the credential visualized in the screen and compare to the model + let actualCredential = await enabledCredentialView.buildACredentialFromInputValues(SCHEMA_TYPE_FVC); + let expectedCredential = await expectedCredentialSubjectForUser(USER1_EMAIL, SCHEMA_TYPE_FVC); + expect(actualCredential).toEqual(expectedCredential); + + }); + +}) //end describe + +//Añadir test con otros tipos de credenciales \ No newline at end of file diff --git a/tests/events_messages.json b/tests/events_messages.json new file mode 100644 index 0000000..5dee06d --- /dev/null +++ b/tests/events_messages.json @@ -0,0 +1,35 @@ +[ + { + "id": "EV_USR_REGISTERED", + "text": "The user {email} was registered: name: {name}, last name: {lastname}", + "admin": "true", + "user": "no" + }, + { + "id": "EV_USR_WELCOME", + "text": "Welcome. You has been registered: name: {name}, last name: {lastname}", + "admin": "no", + "user": "true" + }, + + { + "id": "EV_DATA_FILE_IMPORTED_BY_ADMIN", + "text": "A new file was imported by admin: File:{name} Date:{dd/mm/yyyy}", + "admin": "true", + "user": "no" + }, + + { + "id": "EV_CREDENTIAL_ENABLED", + "text": "The credential of type '{type} was enabled for user {email}", + "admin": "true", + "user": "no" + }, + { + "id": "EV_CREDENTIAL_CAN_BE_REQUESTED", + "text": "The credential of type '{type} was enabled for user {email}", + "admin": "no", + "user": "yes" + } + + ] \ No newline at end of file diff --git a/vc_excel/.DS_Store b/vc_excel/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d58d5f25816ea323596b8e451503f3313734de55 GIT binary patch literal 6148 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 literal 0 HcmV?d00001 diff --git a/vc_excel/course-credential.xlsx b/vc_excel/course-credential.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8ffea58d95418b269ae587cac972fbcf102e68d8 GIT binary patch literal 8758 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 literal 0 HcmV?d00001 diff --git a/vc_excel/device-purchase-empty.xlsx b/vc_excel/device-purchase-empty.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a9f9c8a50278905db99e060abe717c5a381943e9 GIT binary patch literal 9383 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 Ze+f}V9v

9q7<^8g$`aQvCY#e*no7g*5;G literal 0 HcmV?d00001 diff --git a/vc_excel/device-purchase.xlsx b/vc_excel/device-purchase.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a9f9c8a50278905db99e060abe717c5a381943e9 GIT binary patch literal 9383 zcmaia1yodB+xEcF(lB&)cM5{i-Q5jCcZ;OJP=a(f0)mn%AlX)udjUn zxBi)R&OS5i+}EtR_ukhX*HMuN!r=k{03_%k3h3_#GrGb60K4!201osUeQ_s8cS}ci zV+|i?OE*I{ZwLE|!~vxa4ot}lf5}x=xu4a~k$Crl+p(EcMOvrccH7>*>3Mng>}{x4 z;7l>YT&JAyMYf&w?l%j&G7B`Ax*9=ZD`0Y9k`lPFce>}6_N583lxfnt%&^Qoz_f)w zclv>_iI`cGAhY>tTxJ1F8`5AVqC9ul6DbK_v?u#v1`zne#8~rX~(%sU`jl8 zY8t593{-89`}>k8Wu>mcn8Y&+M2AxPuJBA} z7RZ#g0`~p7_3kkD7z+=fdM_zm^j)GGQ9fR%eM7`Vkq@*fd(XQ^A#JN;lhZMO+g%L& z>|%Pg_49Zps-kBQ$$@>5zpFPR@1g7!qTr?1>YL$l+a|tO2eUt1ijZ>W&2!DY&$@56 zUQX-ko4OZ!MNh2vR;>(KKD&oi`D=>ulu1}Fpi?ss3jjQUj>fK*_HOKKzs?m2it<2C z%-09^5X=3!T1|<EL*A5%RXdfC&rh&M!qxvLMZ(jpxb+q}*@rhK96>J;dKv0il#hJ0mlH`hM z!%J76i{y-xkjfc#OTqIz<10))x%vtbB~;1ch^qL3`NTRN>xgi>WJ7i54X2=@86s8##1^FWiz#lT}gtA5hockyFic)7!yd$kZKA=ay4I7Lj~+b8bY>my5NJpwipHcjuL$w)Ub$v_wu!|s~4vYnr2SwJj~ z=T4Mlpd`#rXBXffu{xmh6C*!n?H*hVWXVa_@Afnw=P$~+RX6I1WJq@B~b ziZ#Pc%KF|JT>q?A4cB@-h@-fKqt}---1|j)53cntAWz^$G=}IxcjM1Y#r)I-=5=$G zrtEY#j%@Zo@DSZv!GT%RNKt*;=r$3(fQ7&^LaG#J4Tut>Ysl?B#n8P@?`6u`JGN4+ zC0<{JDbVz*wj-+nfllUzP@290W9pcigv|Vn4xHr(2cH4b%T_YMW^J9@gvH}^lnr~r znrc#!_2|Njq^4jp=a21eM2LtShk^W|hTckDZn45GJ)1uv>k|dU9?sCrD5t?w2 zlg>%zi$$M(j!hoLmgOtnk^PP*Oh&miXxY((nyKHgfO|n3v*??vL?64p*IAmxT>xTD z0pDGBOi3_Z0BU4Xz{QzkfU*~6gaAO=$d~Q(7T`_|kpVE`IO4xSf`6m_t&2Mzfy1A8 zM1m-gogJqxq6wB^830Q-wLu$U5h%r@_vFBx3U@ltgRk{tPhnmJ0^D)`4vcUBW1-EZ zVY2`g&Y?pymI@85T;wJkc4VLjRqhy*_lJ)PG{A{If-_w&6$#)q_QxI}P+wrsEv=pA zsy3!1*P;v^Q&+3tOCd_vrKw{st%b-N(Oi!y$t0mlcrzQmJPW5$wX6e`sv|h_d(`{Q z$a>TRXCiSE3pn>{!J0_7&ppN2Y zdHp4R(1wE~^d<*pDa9`S7;{MJ0xx#gk0&t_RcGN+`Ipz~)T+^6$7OJ>Ux8^Lb=pLc z%X@WF`Kf*d@eKR(wptm>`&m-?pZz{SKi*Hs;CemS&_EhrK{4jO(1og0c~wSIYTRb+ z1ivE?o|M3vK-X{5iKv;)^W7aqFs5jTtobQ(nC`VKggcfS9-=>OWuaX!;3i4%L3-JC zK>0%|o^}&@C9>&q@MI~RlgedvuFVL>Fal}M?V`*`fjF8_c9Qi2;P2hY#tpicg$7(& zC|f}{RTN?9S0JU~KcB7mDOdmCdkyx7b^z)Zyf-R+PPO24aC5 zclzDfxuxC0*S5zQ&ch~T3)~{CC^^Q9u++$1FQ+P<4gv*7>K6NL2A87IwYr9?`Oh!G zgYn;*JO#(+KSfbETgISyagXH}vnN%dUC``NI#)=t8N_T7gE~!9;n7lv*h0hZFgi4n zSdP9W8ivdmQ{aIvYQL6S?&RudsV(heM;z6^TtlmPsn&|?buTLuM-?7gxOEg~+IZgl z))o_-DyCu}-mFjeaXIT)U(X?b?8`DuY)ziq=8Wv_+{h@!G?=}P<0ZO_SjsbHCb^(9 z^RFrGlNJ_Bx7*JOgvUj~eUig~!k3cd2mD_Z;+7_|)UfV~+h#_uHD<#K+Z`o%V2eu= z$`jgAN2Unq!90jsSc^eT{C8%TNCdBoDZ2Ro_If=x>`KBt&Klno~+xRJI!MLo&52hwF0_oi) zPkZFZ`Ai$%q_p3wsTz4wW0a_G8a+E%Y{Z_+H7){Ig3>SevfaSMNZHqASWRQ<9NRuP zwtmTa9;6@Y&>V5j>39v>411l97J5Y9M9`T!3vPd!hBq7u&?s_tQ--+2UYw`ER`bEPXvS1f5DUdBy`F zHbnDbZALP>%%#3K1M~g@RoYfhgLkxe=~A}~e~io5TXAR>9e8L@%G;02=QflYS61v< zXo_akdy2o@_A!r|zd6Y}c>3c=zLZaAciej?(o3{J`A zQf7yT)b=FCEXU5ex;EHWv*TX9bEey{RTZof)y zC?aXuvSuEa*thHKT-lTcEzBxAyT2l0Ei+X-2G zktCj*4|rEi<>8O8PR2~qh&l9`OJz-vOa~tBJ%L%p@~-&0l-5#TJEa|6WyCZGEl|R+ z3-!4c)<)$CGqz8y^Jw}NvstbjRs};4!Fz(`nW*+G)(^AsQ|YoS?SmujaPh1W-H;dk z&-x>FsN=m4V&NtSEv7Xfn*@c5wUn!61$iP`0 zgN~|Z>-rHhiO)~*LVCRuBnN$(*J6aR4Y=DR_g-HbrHi5BhsgLOfCgh&*w^BF)2~<| zP0ypmZt+5Nd=h#FJ(}0*gt7IxTX;+*sD)e14& zx+tu&rCiouCtn=P*bHgeeM3F-pbBG|G{a<7F8Jwz40onQhFo!qRe^exgb~IvZHCFL zESveIdadXeI-~NC26LMME=JWcYAsW?RN9IAK$H*7XDhSpbVRe@cb_&t_+0&%>fd|y zH$goqvWlmLCTSWV008qhL76)_I6x~gw||neAF1+A#FUtU2X~-?XGLnWioWmidp#4! zBUhUf`IN3do2Cw_ReiLisJqz_djAzn_H0VA+7qMR#ANDDdy@gBQdch{P`(Ein7A-$ny9va+xr_!7x%5gs*rRq+((I?sk zYsu|ba&VQ`QJJ=JVyck@OmAC$&IcrEi_O}P;&3JotLqb-9_zz9>iHjJ6+3If#7T+@ zP4$_~I}~0|^Tpvi_h;$MUqF1Wfvs1)dU#@OUwdmhG_V{P@lI75^s| z!>**S-NJAvx#AOk-3a3hvGK@#CIFdlvp_(3f~BZ~edN&- zA@S&e!QVX1e!g-BI0EiF`RzCJSd)%5 zDGlr`aSjWK2FUsv@?(^pmu_Z3Zsjz*)8^xGcV=ufMp~)N%R=b_-MiF8iZpK)4X_VS zd|`UbP>kS0D^5a=)G@V!?1;ilJxqIf-2%s+?lly4-c7-R^X1%H*HvP;zUbMGw9(O0 z&@31;^kc)dl@xT;Af}hG8?tYyac~*71hFP}`ZKbC&nJ1)hREKgW6CZ4P+s!Uf2zOv zM5KIUEBHqBr$BX*J4|K`+daKGeJTP+f z-g?-_xjUghHe=P_S-H*Sr#H>eRf;jfj*FFFAvjozAD6lc_0_RBOzKwb;BWM-*~_0~ zTId(FIsLt*fa0ApR?tj~0vh*!H9Vl1mYa>KtEGjyyX&vO|EuPC`F0N4+dvIF@PRO0 zya&>Rs?37jDL$vs@sUx;4-zpditR?9?e*Qi$=zaGIp{o>-8HMW^Qec9>rYRZi|%Ft zQoq-*Qm>BtF>nSPc=I{lKp#lE1DGysV8UdK(nd>uPw5)x6Pq8DywdxWBi>(c!4)Fi z()Z105t|rb>k%MCwy?pf{o1k}L7cWRN;5>&7-oCnxn^z|N?GEx9<2%44DTgXNm=j3 zB2pjr#D>a$ClWOOlu^9zC8T_%V^;pJTHXM0n|i9MhX0gp1^2j`EPCeZBnB_aTT_V2v7 z``BClGa+9!72`5EFuz`C>oi*~O=c@`OVt|7YIRGsL(p`N5xMAs@6JeP;be$; z8=u*uA9>%de?8);f7@t;;%-sArdK4;tZUw>k;ka53CTgf`N1kxK?}jo1IZt?>iMU9 zmDdwOI@jU0!s2+x*Hu?HRKj2zl`i{ze)Bb>4r5(>XZyH@>MVhj%}#J(1mDgXTS6#v zCT-bUKd`>WrqVR76CFNE2S3V}JrNV!&3)c54Uv^iDtkY4;>Ax1u9Br4(ghmHiF!kj z0~cot*_-HC34rqMVi?{G#S?v4x&u{->cG$R!5goItYvBN#hslpJG4zZWh-lsW?oR< zZhV^sXSq9C6IL?p5RUJhV-x{P>uB)9q?ANI!@5!qi=??($cn$Wl-e^5mb&5K`A7?o zhHE3dkJ7+p5y3wz8A$$|fAY0h2U}7IvpgI#D;$B*569Yu$e;OjdciLQOKYjxRkNv* z<9qo%AP##OT(}T-t+*U&*WCC!rilW76z#k~K}#PHu}R7W7m-SSF!Ir6YCw{Dmf7OY zK$om;qgy^54n!zDh)#jzA%WGI-Nx%dGV0oOSu~1F`Ko1QMUX7M7^_2^-p%U#e*DJr z;BFh(-T&3d@}=UM)5KoJ>OE|d_D1ui?U6k9f}IEF3daMLcM*+H9hsn_Y6aS>*D78g za2HJw8u5FN+kF-}E{k^ts!ocFPguPthTr0Uxnb_YaP%{ax0-h`{n9Yn-zt0aCF-t> zRuW zLQ}go>zDVospAarn$Db^BVP469w0v+Dk1vDLW(MdFAz&0rS@bDwSoEY#8g>}AZVju zso-06YZP1)qtmgp^?Ub#H-W*qd97bBJQW!ujF`|>33fQZG3r0V%Xi*^XJ7QrM99kS zf!n7@K{(z85nr7zC-q0}TP(~wtI&XX;N) zi3bLRX_0>tyI`5a%VW(1wf3~~S$TfdvYX*7jts*l0Zz^qV4WcoS+G=85T1~Qe30m~ z@33AT9iiRE733ojnvTotXT#Y}M$T$(bg{G^ZZSGE?mqMV>Nt{iG5|Am(WYjNymPLvWZkU}1f>i# zHS&H~*E%A4=*A8x7~AqD2Kk!@d8Hgy=#9-)Y9H>y9grO>+i=kw zVPE&-Lwev6T4w0erGW#Hhn)=N(H!NLmsgka+(z4%3jw#Mlg7;)z$A44qaSYGTD(f#qD3Tz;WBH2T?syO&1_UF=yW9Z8GJ8nfP(wjUIafn&% zNRleDQNZEiL4Lr4@Nn-=w&>usroAow^}$Ahi$u~#rOjjqOA`dym`{( zmffEih24xVR$cvFL_l7a6q6w(9i>i7Kga zh%B5~SwDYnUe^<1)|i)rwqbv)$>8xKCc3Smok)9LDODXXnYiw41)fj)Za`C0_&Xnw6TL$g#n$pq&~nq0 z2sH;rxpP;+>cR%Gp4=qu3zpTDT89{ugx~=I_y8`u=c!?`v&Wj?e!Hlv+P3vtcUxw( z$_SY-4Iv#Vc+CL2^KBa4P2-E7gBbYcfqBFgC6Wbew^4diCF1f@)XU`{p(snAIAvU{ za8>8%*Y;HC9GiQ1(;yY(k?3oC%3QHIuA{OfW39F=Z5~SxlVqvSyRxjT=kUTU**kAO z@SN>}8d~xgW1jGI`FSb!ap8AQNWCF{XM&1JU@16L|5*hqoGnpm{NM=ziK}idesFcK zh8T62W36>hTm7#z~&duhY~gb3SlOS>~jbGmkBM$;2WN zTl|oce;3+==I&h;p$3ds{b$<243*blR>J=iPJjiw(i+d=V57w zIeV8m{TtPc_9dXD@#yE$!)dc-(nDZ`NV;+&684c=IZMmcI;OiLxZCpm{WPa7R`3MG z+j#Y`RIstCecC$8&&}Eo!Pw8bamI*ERbQxgwY*3)`|?nvcRncOdnluBlcFy`dT4)_ z9k^)DF7xRg;ctSkjZncEgcAEFXjG*BP4NFE82r{&{>TQum6bOGwm?qQUv0~RmlbI& zatucnI4IM}tH96-pV1@Wfa2ShIB^CGiYT^*xBHUP+B5be0b2CKTwNx)KhPM^`5G8e z5n{vRCV3ecaY=?aKL$N{V&9lDL_eaj9UZNXtx5R;HM*$KOQMR}51SMxeEXEskEw15 zNLOvl!%>;Oj&?!~Y=Bqa%L;1pVtv;^dt^LSbwEGRXw&j6{MZ6M`?hJ|RNbjuqaTP5 z)GKJTiwh{XL%S3n+ZN@X-gHW4!M=l3h%A^dE-%c1{&vRVqWUc?G+RZ5RuNc#r3g;0 zmj5IO>Z(p-9GC$c+B!b4;EFNyJVrV0wr)6V91tt)(fUkE_h41b9?w9_#xf6>{yNlx zQE<*{!E56nb3tShLelC3K4sD!919Ixmy-EvOuO(2qb4KtvbENv-$sX)^m4V{JIO7> zWyNGK8Xlj(~j*RFj}@ z+0It<-K#Q;ml9y&5~`p@T-@>8kdmmY;Ovhu27Byyor8qc$$2%-SWoHbPistyKJp|wT-&RK!3>WTyfQYND23x;CfbVK>X)+^=U_>5+YkmXJFnP%UeL)B7JgMCg@`-8?=AjY@;!22Y2I2^Vg)j8(aMAU zPZ+XQ3Mo$3Tdv9T_G3Iz8`=xqQP()8FLAG&y5lKgyW{VHKApXg%94i%;+z z!HqM=MU6<)k!@+7QZ=w{ELl5U9~xWX4EZct&$mO7e^hDF#bQk;u*baON$WQ!GIwu5 znBbaJJ^!LlDR9?lKEmS^hKjJ2MNoi6R~#2Fv%oP|Q$XG!4k>_&-*5k z+Qo>GlUu}a>Y(!62DAc7AtAP>*T!T0J0j%G4$72QvPYP~yn3p>g|`mwr<->DWN}3A z7srZaq=-kuZ8mA#+DeM1Y%ERFqAe>}f)|ZLy%Bzl*jFe>Fh13D&+Y3$9gSEUm+dOUu z+9HpP0xwenGc5d{>gfANd41Vr-zjnmaRmqXOSDAKuGmdoCsBP?I7!FNkdl1&9kv@B z7wWB4mA`j3AoYb*1Apj3s^#X>$wP9Ne{u=;``dQp`?I7qdKGyXSX|)${^SF6s{VNe z2K^fTAJ0KN26(K?|278z0)q;m)Bj&8{bR$&g7qK64k*$5@ACCyoX5iMADk8F+WS{n z`X4gyW0c2&)E^WVXo3DO%Kw$o9~(c`RsI-nK%Xyx8vk!9%VVI&{m(x@5zrn16zH#R z=ws{0t)4&Dv(POFwf-NCpT`J~n-YHz+^~Nm{HI0n*z|D``N#Agy4wD;{r{Adj{zPR zRDS@t@&5Vk$K};yfX4y*4*)L7KLG!~@ckI@anSn%NJ;wNf9c;4_}Kh01^zKtBmXb+ Ze+f}V9v

9q7<^8g$`aQvCY#e*no7g*5;G literal 0 HcmV?d00001 diff --git a/vc_excel/e-operator-claim-empty.xlsx b/vc_excel/e-operator-claim-empty.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2909e10775953bc9abe65ac731c2b6a8c3ae3bbb GIT binary patch literal 7956 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 T6&ef-I`l6ID%>N|U!VRDO`7k6 literal 0 HcmV?d00001 diff --git a/vc_excel/federation-membership.xlsx b/vc_excel/federation-membership.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e2cc7b5851636c68c550c26c805ef34ca79e2c0b GIT binary patch literal 8606 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 T6&ef-I`l6ID%>N|U!VRDO`7k6 literal 0 HcmV?d00001 diff --git a/vc_excel/financial-vulnerability-alienUser-with-user5Data.xlsx b/vc_excel/financial-vulnerability-alienUser-with-user5Data.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..88909c1e27fa6df30ca6f4490111a59502fc95eb GIT binary patch literal 12947 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 N6p-iaF#LS>{{U;2Lc;(6 literal 0 HcmV?d00001 diff --git a/vc_excel/financial-vulnerability-empty.xlsx b/vc_excel/financial-vulnerability-empty.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ed6c91aa2f2e867a5067c16750fc9e07dc33e12e GIT binary patch literal 8290 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 literal 0 HcmV?d00001 diff --git a/vc_excel/financial-vulnerability-with-required-empty.xlsx b/vc_excel/financial-vulnerability-with-required-empty.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f8f6cda6bbfac7cf4389a9be82e59536841892d0 GIT binary patch literal 12507 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 literal 0 HcmV?d00001 diff --git a/vc_excel/financial-vulnerability-without-required-columns.xlsx b/vc_excel/financial-vulnerability-without-required-columns.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5b3a35838c35cbb932cbc4c736115b39c9ecec85 GIT binary patch literal 12434 zcmeHtg+( z{=R?U-kzs>dir#|&&-+kt*$y%O0rPU7yuXm8~^|y1z1ue)dfKS07pOo0384asUvD{ z=VEH-qOa!ZVCwva$-~x$Bo`WzCK~_=zW;yQfAI{ICk!iegHXh8q#i`K7-g1ggy7hZ z0=m%Xlm$BG;#u^#~GhhInMJoU`#Yxua7 z(+yDz9_qDOZ{x@F{zX$at^5K+bEXMT6nNQp*#``jDOiczSl=N9VaTJ0Kbez{+~qoi zMhhVU`2_46P)vE=;kdCIteHmVzS&aoP}1*ImAtZBuW}1Twsf6Zo4-jUc4gk!+e!62 zh@lD!Jz(D8f=oS}jzzgu>a>KbyZR9>HMRX+C)CWBR%xzg8$NJ`bMuCk(Sa!R>J}RS zcz%WgDE&pe=z49YkYMqyfwhSM7O%dOsf{x;({IQBiT1x(sek(E<#F<|P%J1RCjrl- zUt4*X=*T8*n8kPHO&%dwQXUOBQ)2SJJl;qMyX1T(@=C9A&-6^WGONk+$U}MShL$A$ zA|Tyts#&8;_OY=M7B${U;=`d-mp_?J=|S1ms}FSBIrCQn5#`@{JU$EzDR58(ppW0iK1wOU7Ry;o}8(vNp1}xBl&k_iqJ3fFFBcdHr{9VgY1*`fLY zGTdMt?GhE~E8ojCe7Y}hG-qX*q3lCEPe%IPZrERbgF?URq%V#@gZ4tVIpGD`k6esE z!5JM@5CupA(a{d~56X@ZUZy&uHwnZIFl1tIeZQrl#0||WBon)AnhasH8U?wsS~-ss z4#{h+4%cnH`w^siSP@Xy&_I$(YqtH|B)W7BlNI1eJaK{hLYfcB6ELwmO-g^;DSi!i z-tu53?>TDSnfC*6o1jdVsFa=GJH&+~bU>2Wk2cPEJ2$0S{ba*M_2@#Dznr-L=mFt`YrtkLuuyOH%%y;k-3!vl6N$9@au7TQ_G?W8o#q`&=9%Ym~Ny8 zQJL6Q8Qj{4qyj9tu~sIi#EEEwk@L3n{-S?dd#@;1MG+3*guXGoNJi1qLpzqttqL3? ztx@AK#!B)Mm&@Unl^bdrp*Yj!B*8`JUL_6s@e0SP34?{WxbdrpojXaYteJnCw74N& z9m5l%Zr`P<7FYEU{5wrJ+QZKUVXV?>cs-7Th>DRxVcWHl0v>>cT%^*|RKL{m^X*O~wJ z(}h}I5VIET5kcf~N4Lb-O-uITi@sgyVb@WHld|Oi?Tb2d$mq z9^OW-JC$w_5}$`C`P@QQdr5e%fbvvi;6uG#ndWdqp5V$}?@Egtz;m=mq(tTjmX@sU z-r0I7pIAI0etG^!)=w2&P@jRFUlKF`fbo;{&K8DFrY5Q`PL_7&&cFFd8#NufB@oJk zo&T!vx*s{EJOyo zi^o**@+$@i-{oATG8gDp06Y}E^?PZMLtquoEa^SB?uxN4Gd#bI7M6z$LCyvKN+eC+MB zI`Qjik}wuA&9j0ym)^54zW@3(#+*@@`Dg=#$WWVAK(m^M+pA!*E+Eev&ooM<@pBmH z50zZXCVD~CREmi@t8v&mZfyRDD+p&SdZH*}=PuN2G7Tw-UGjAr)kV#fvmH5@sp{Ow zA;a__COLl)KL64fL(-T_`XJO!?VkNO~ zpjUJ`-o3Gnx7#IE0`qwbS8J9R2S|PE1Y&?D6X;aY6)<+Z{rXT3N_@S>E}eLWX6@V9 ztBBUGV#^LN+G2wYJ`qnE68!zyoG7qOb=ri{WHwyw&p0A;fksMO;=Wh))Fr{ioyjzD z;n4lY(FoLT&tfXi`X#BP9#TQd@sm z3KZLP-W@0Rrz;n?ZXR|rl?WMzEfQ32y`8T=ZgWy-SJP;* z5K>WWn3l}<+dV?2Ul`6JuA;;1nmP(=K2T_&Rj`zpn#3l^RA_jmes?^R37pjIi`~`~ zwKcjO>SNHwk3TOV2`h|seZ6$r6kmCyO@o>%NB={PY}mE$^U7R1BQyuRM{O#UT@G22 zmdUn{*=0e>@Ye#W56-7lS`n}T$H47GZzBWtqPT>U#zMApk zx{@Rtf8gej$_#S?odXTs^h0}ie@X?L)hARvD0ObmbF*?at3?jJi@AmIWtsCv^1-N` zx?CH2@0*;%Ol1LJ0>nDwP{$`ue!QV5vpIvub{behWjDzqnr2(|1>QVwcp9iya+;n( zx_cisfBeSXA+Pz|L&A4k(vorf?XB$&m|OP|HGWig7*a4+4HTkzED(j!qBVYixFSO7 z>sCjdx2ScATNAijssP;il>k1Nup9|u*ezxE82)DZ0sqC#qm^^{L^IXcB3|CMv!AyG zS6MO|fHEwiTqWPm&x+WSHXq&a6_NKF-(=*y&n~Ek8zsqt9O$$Y*T4`B7f=v?`BEu% zgR6QIH@(aCnKZrXOO%iN`Ig){Z4Bb6-CG2%`csIb%^1Er?6}ZV=64JB|LD(2`iS@) z!D5Yh0RSNV%b&Y=+L-?C$Xhg)qLw&Ne3)bWNNy`sq^IJOA5Gs?hFN=HC&ei|pTaPN2^)shhK3YFSNE-IsfS!j zN8QN@#mLw^BCVMLTnF#D0ur_BOF1~-9h7}Bqx6w>->&jj$&3Zuxk?#+gPo{de)}ks zKR;ZOf>NkclS{AK|ObuMU zr`Gs;kInpj_*kX(J8fO*Pz(_8Uppbw;&NN8d@hnwoc{ia8}nGd1sC;PprM z!-&N?JcWGYX{TR(D<5T;@Rj41iDWm&1-Rq0kB09F+NLe4_qSnNB0j8AM)E#MU~!;2 zuQM{KqS(+_x9s<(UPgDIG?^z@mr^7Y=gI2}IcIzb^PQPm%agyzw|pa;*>xJ9dL)Qv%yV5VBihPtr|ni*(0u^`^hk@Lc2-z-%UKiT)GI2cJgW&5T?jLVE4^sI>kmqmZ#pL5p4< zh|*|1zZz;&0x|Ff6|7^v4B0r zxKnD%hYS6Z2i||wW=q)%mRayfBRJk7|EbO2G7M)6Q&Sh`f90pYe_;7fc3K`YYz4)F zD1Hg;1L`Y@cy@H`C~%z6PKdE!9~ak{S_hEn&>x&D7Z2!)csOjHGVyObHID9!q40# z`^_iw+(NzL3mC#wJ=dJi^UocZdkh!raEoy2`9W^vXhNOpR1xwrZGJi~eH5T1Ox$60 z2-+sFG;4283aJa+%@7QrC@`ofb*yisaq!xJgL1Eraynw`C*!mpN;z{DtPN81DK0=! zxrQ_;G*Z%1m*j}`dI&F_dy&!Lbwt>n{-d$t`XbiQS(N;9ItojH(l zjf*^rx5nb@#Tl^RQ`k@WYBlEA{hSo>C=@TMOY0yaV(EyCkgfa{{-8bx2|sy>DT6>P zDQ->qYTqFL80`)7)jTn4ni*v4PANQFx$3z;RTd0Ix_oP`9~@Uo+I>gO%H32_kn$Y_33p}PnCKZUeqj;Q|Dt4wbg|J$|t1&^Jl$!rhRz>o@ziN1r%3|aTS%Ga{-&r|9-_8G5# z7wzwdgjpTCKoAOWyn+Nv31m+uM5m;u1tgPtlqq{>(R^>z0imx*d?&j;5YzN3PpsQ| zE!va!v$V_G`*FMKQ5Rbe`>O7Ty`JyPSzj%ir?%-&LMz*&s_g`{ywF^i1)K>EK39&+ zh2HHaB*(H)#!lFhsRQH13*lw4n&@n_T8g`3#dF|aV-?WxX{nW-nktu(#faxcC}ElB z8ogEiz+LcGZI)cwu_ga)bc8S(tuRT14P^^NVb2BB+yx=_Py%|tfgW^c2V6inDl|Dc zoEa6|v-Bz2$QhB#wN`@BiO=Uy#%;E-{%6SuO?8DXo%?{- zhUUOI5IXn{@cNL8g{iIS&*Sed`%p{P9-kAX4SUy*;|HU&k$(YA1~(`6AJL*vN#xEJO;oEgA~O!<~0z(Ya~Tfr0;U{t=i76 z)(UHYWbHoJ<`sno)*^EN7oxB=y=<&mMT3Urm<5*5^#SXVLX%!)(=Fsr$J-~_G zT~Ubi48mTHh^ywf-zc{@^kRg|@f~k^EeEz9)bex-Jl3(|cBt1l+021@HjwP zR$Hb~t$!?0GsPRn>(Jd&b#!05D2MoJ30fLo*f%Cd>Xr_6O0d$<{diR6n=Hc!3v zq!uKu`_+d8d108tD(tTZkg+FZV>0QO%0X(C(I7VRdjQk0xY%Z7sr$Y0;`2 zT|H{JG$#rL3Kl;KYes0MkA`dbdHXpwP%bFPuB(lF(2n&ra~n=6Rw!dIhb5;?gWJl- zJN?>3vJ!@|^*>@4;FMnGo^ZVJa4Mwo7Q>$<`f^LbL;=jnzHZ z^62gm+hK-`rt2f7-a@>I=ye-`D@&ddA4M_L7{oovkOq-VkZ0Sc32)nyB68p*P^~Uz zRNo!y=2^fMIy#tK!p7=+5;+k;<)C3kUv|=0ZdAZCVN+>^NuEj@6J_O`*qJq4+WBoc zb$pv-_d~}J63%Y@A?V&fB4qK{O(LsFytYjVdk_a%`k1`prY4%MPr6n!B{;TU(yNfK zR}yF^FMmzgn9(aO?pBXJKhko;q?d{`iNL%^AdnQrz(_?cJ7Gv~FK3XQw3y4;X{3|%kiDXS!R481W*6W0nk$RUjZGAeAN|(^fcKD)E>{UHf zF0j&fg9RFM*)r`pfMEBTV}}7Leio!&%8y4XyuqhZO=5yQ6c^s68m4|;I~-@iO^U7z zbTl>VC5nOS`tb#Z<8py08%JbCExrX2XZh^b*>4AzEzBkULh;DkhBd2!e|*K?w)*ht z)iqhY$OJP1Q2u1W+^f`++oQYFcQkKSqnz>sxF$wikE=^Z`UpEJNGBp8hjRc`KX#M- znarcgaETsT*{7F-ND5m@hX{6@Ye5`eE;Yec^ZfX_Yl)`uY?OC}TYq4(Vb5jE^3c=y zdyTWx*~rSyU1WyNe6?%!)KxNG+_gu<{K=BYx&@JIqhQoyOwTOd5tti=K)1RRPz?S-&B;)tq-meL*uap~VEP3DCG z^w*i7oAamND_9-Jl=LHF#HW>R)^-XMh zN-_iwsqqhz?tlV060qvnx3#JErB^ne49;F}8Xmv_&fm#&**{Aj3nmgCcwq_U&tx)maQGjSehU|WZs~FI_Ddkd zkQ1nDV#F1E?iD3O|8gU#*O#A+AmTooD-;^%)KG*)#vaZoex8_Ka9jL9%&aGnTWlAd zLGLAJ5?Iyaq#Oa+`l9~oUWV`JxT-BI0LL4N$)mjIh4k`%H1RZ5a9B8_N-QIvdah{{ zJ?&uKJOrcD(vAYU3T*mR-Nziu^3G<1mxNwPbEcl;X9K&|W>mGFecd5L zauE4#A%jFzm%1# znkHkVO#!MpBJM1D+6W$O(+qSLIDnFcoHo%M%@;@?EjVIsiF<~*&jvte3S2j^JB6{% zEoZ%+SQoSiNq_CORjJ$CsOByyc&@ zPi@EV#^h5hs{F|L{#-Ss-Bm;N{IU4zpVvl9NOexd!4+OL@L~h%pJ{At?_~Nv9{-!g z06?VLi8Z+3_b4#`O!B}Te&oW~OTnZgJ8xhbY$7<%DgK3`VJI$kRQh7=go4_V6$T$C z!1ZLc+1+<&>$rf$2tgpbcB-5JGX+qVF43H6rjK2_VM_%mT~^?TRk%PT;`X}T=asT; zL3$|liS}s_3iPW5r4qMP+ptBtvA{(k^6oIyr1$hY_|gyibDs1wwYs7LOc7}<5OLaZ zU&%K<@w1r0%2X&M*zU-ajnlzGA!)*Ua?@SBX7IP~(#KcXMqD5{(z$lQxk=Y4Yx59k zhvtKDZ(gx*PwZR28-D2D-0`g85yN*cASt8PRu3}C&dk{+Pmu6Fhy>@s7MhyNZ^!ur zu&M4|5G;u_JOCs1Z@#TnKtc)TKrs%ysKaY0=CurP&?`6TR(UXO?9o{X$VMW`%A#7- zldDVc_e{ss*_&|ne}TbG3}X~p7e3NeFcDL-9KlU02J6NclN)A&=*6hLw?IPdp@h;Y z`{|R2j4JR{M-4?{@Or|At&&e_woGs@dPT+itYu2*=~I?53(N`bHSdmV@qG{ywD1(3 zG`JGmjgCuBea8)&w1yipmQBA*{W#{Zg8z1LD&&o`fOwojU`AeEVXQyVonAT0WvI_1 zmm;KJdW{^smVNLYH-|cLjq&#V%wxAv(#)pOgPmq%Xzx)jP3h%BWEb`$i^QX_bIcQR z-`PkmL)pQBm2M^r(TjZ-=Y@yGJD@ z=*F=zbr$sM(~#-nG1`-9E#3$>g7TtV(x^xmGRm#^kB0>nZsU%=4!DkxAZ(T&!6?F* z1|~SX$VpyKp%M~aJsEIB!e%*uutE&;_(3_G62=o0EQuA^9;ud+hcJm7*8K`5SH3#U zbliH)lXB4}M%oFyx9fN(0Q`z^;KzOzh!-Dsv^z5%3U?q4tME-dz9gtK5QIEQkb~Ixy6bt z0r?jZsizr7rYWb~MJ5UKgsir&iv3A;rl`W`&5ds+C9p;ZsFG##hDEXYts!&8-|v}Q zzH3VFg-jM1uyCtq&?^3{FWg&@odZOo|H9A{dr2I$_L5pIae_cm@%D?zJx)4vsI#vp z-Dp~1d&ZV4Z|_+LQ$}fOmgnhVJAP%)-QZ9wt4>Ge`KEO>US)5Ys-@%hz@{u!Eh1@9 zdqU?`pxpYS!+tHSQvV9aMw$9eFK7)xfj#qPWCLkNg29C1`C`@3#@10k5o-|`;;H>A z12ZFSb&5a`ac=v{kF~!D(~Ms?`K~1B4w_YMKDD@RB%Z+vTr@e{DDWv*9T?p!&a?&wj=EwFl`BByzBBe(6a1)%e#|jz5fX5q~%S zTVuzs2)|bP|3Kh-`8&e@RrmiD=+`RbA3%GkKflU+0VhD5Bu?z<*c^{1xD@3E3a!0Kge30Pqj#*{=Y94Z{Bn zAVBdafPaPLzgqt__W83lkor&7|Bi%yHUHIp{{i4n_xsQPi!)b}1%hc00K5dhQozdD JWccmV{{ur4J39aX literal 0 HcmV?d00001 diff --git a/vc_excel/financial-vulnerability.xlsx b/vc_excel/financial-vulnerability.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..33b8db05a4dc5a60d3b186672d3b35a41202ae55 GIT binary patch literal 12529 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�hwYeSE%z2wFOF3aN2j z8k3pQD}XbiN1_iVluEo11KC$(~Cz*BL*MW^HXaKSrT8XFqDqqe+CB^V~6 zX{>}QM8pe9WUz!3qSZc~a$gMwNie*9YohLf*N|PS*iiH3~D=LF7Pv@myzw#9T5>eeuEHdtuw9MU0&Rs+91)CK6cZQLK7vfIScs z+@=(ya$YkSW57v-t^j3gXw*#*1<`r`6`JjGo*)ZHa9KIF1p#O2?AHGCE-p)uL+pjz zk-G&`W&`iovbSZ`;WP6!ah>2e13r|@$-J>!i7Tf|SBK|F?u=>~SvaWqMp%cdNlo+w zGxCmFP*fV{0HSX6Caa#H+@tNl*3z1`XH^6p(&`iHqH$EwMzq}MUmE+IdzP1vSzdXw1AyRo(1 zPXYL?P!%ZT(h?6p)`&ETI^%S8#QD!Z?533CeFo(wf3F}9&qYsQfUe|%{y~!^U#Ojou_k(YB7xGE4ZToQm`q_9dfCh(8dtI<%Rd^Sa{*8Es5 zF$5I@i>Z(OxHMYw6S+YEoi%#M=Dev;S+nC<93kn~n=Hs@4s@(Cxqf9~IJJKKNvX-RItG^Ci__m7Qs5mV+(GeWi^8a2-`1p9mza(0kS-hHG3nQ^_8(Wn zbYg|&A2`$u5;W-G%um{fpxV5d&K%W4We+g4UiGlqmY5eaR&nlPtipfaKU$FM(t5~` z2Zisq!HXM6khdu|T?!<#Ug4LmC`)^up|y9@YIp<%IDaS8W$z40G>AyAL7^DRpUI?a zWAi^I{T43%aiqn_ST6z*0!|>V2@#g@IG5!Syvy{&)h;Xbz+&o+WefDPt4V{xq7P@~ z>c^+%ofg6g8MOIwimW2jY22g@d@H`&DTIKxzN))=kmNZ!u4oJL!Er~VcPab+N@D3p zB;gdfe^4-;Vl*9(N{(R!4OM^b92lM5;;t;ZB23z3ZDO`*Sx56nq&IE}vxcrDXMKC- zWfdFq^3oBd885T7NJeY9yUSEYAIsZ%_R7FUDyAZ5^7ymYH&DZ_@Qdi$-XL<3?J~u4 zHkcswpGzc4jwsc!uGyrE?Lp&b$DBZ2c>&9;A=si-c`Mvl@D&BF)wI{@C_TZ~0jj|S zF;JtpHc>MDIL+c|mr~*tQ^ZVE$xuqR2)hfe7Vi$WDf>F|ZGcJqcAIFn#`8o^CTvl+ zgx?1_&-#F8vK%)sy9LqqEoa@XV}vsYV#d|Rx7c>+NvwN?*i1)1WsK#G&sr+z4q%=7XkMk$s-DNLij3;c9lUZ@G7G`r>! zk3VKzNv3Nwh4`pL4qBjN-p34)=(ck*8o@}G%f`tZ$PkZF!$7dA!`;~chjgkS+mJq6 zipMh>gx6ZnP3kV$Ta|y^c=d#K3b}OTV`mCH4t3@+MSJ(DL;Nn&@X^X zRWYnmXNl`*){gK#Y+ml3DJGv7#}%P>Y*j7t#b%+&<+t4Tg5OM#v%x2d3KiJV?D(O) znXBUuA&z0(t24-Y$S;y&+Pz0_Ivg)GEf zmuEybPK)u}N!o1o540I{NJo8A0l8CTIfJmYt=(_p6-1Pgh_(|n+I(G(#)qbIcuvE2 z7!q8FP7x6)qR?(zpQ;4M2(PC%d0su*4lq#}q+{;v&c$iuUdd>ik_*%X$)s;UTv1PTh08*pW-LxP%-X7EZ*Ay!;U`+hjVlfvm-y4|TTMB#}bI z7!^?MsIbMsK6A#~%igQ4c_`EEKp>n zGn&_Av8Ts}?9$Z){xP~+tj-+n; zP3KZkvlmYAY#rhFp|)u2czi@4Rw#4sPHDL#B|9eB*Iojb$Of-))jc1YjU+EW8o2T` zRnJ&^@5;$=CY}#{TnR`*tElsWOMVgW-KU6*T7d}J;&-L5jF&4_GVIXU<0iVQ+IwBg zsUArzgCY~F$_&c?3eAeTt>_^zWy>WC|2DQh`G~cqia-%wu9a$uWz0`!S+zTvfq+0 zUBjP`hH?8W-yJpga1t0{APyU(>Lk z<^aHZVgTSDlCoa`{u+A!8K98#PXPZ4#DBH^YsB+sYb%OBS^qo!`PKYar~MP)2lel_ a{}&f7F9ijnJph0N`b!2WN09EfZ~qT}+jP+Y literal 0 HcmV?d00001 diff --git a/vc_excel/membership-card-empty.xlsx b/vc_excel/membership-card-empty.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f6eb736039c732aaf1b1127c28f456e23b802a2c GIT binary patch literal 11954 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 Gv;PN~^d5!) literal 0 HcmV?d00001