-
+
From 92055f30c995de0d63f25623d40dca0958fa81f5 Mon Sep 17 00:00:00 2001
From: Fernando Terra <79578735+fterra-encora@users.noreply.github.com>
Date: Fri, 20 Sep 2024 17:20:40 -0300
Subject: [PATCH 05/28] fix(fe: FSADT1-1427): Autocomplete emitting value twice
(#1170)
* fix: prevent from re-emitting with value undefined
* test: update tests
---
.../forms/AutoCompleteInputComponent.vue | 21 +++++++++++--------
...FirstNationClientInformationWizardStep.vue | 7 +------
.../forms/AutoCompleteInputComponent.spec.ts | 18 +++++++++++++++-
3 files changed, 30 insertions(+), 16 deletions(-)
diff --git a/frontend/src/components/forms/AutoCompleteInputComponent.vue b/frontend/src/components/forms/AutoCompleteInputComponent.vue
index 00bea9e439..5cb94398c7 100644
--- a/frontend/src/components/forms/AutoCompleteInputComponent.vue
+++ b/frontend/src/components/forms/AutoCompleteInputComponent.vue
@@ -90,14 +90,15 @@ const inputList = computed>(() => {
return [];
});
-let selectedValue: BusinessSearchResult | undefined = undefined;
-
//This function emits the events on update
-const emitValueChange = (newValue: string): void => {
- // Prevent selecting the empty value included when props.contents is empty.
- selectedValue = newValue
- ? inputList.value.find((entry) => entry.code === newValue)
- : undefined;
+const emitValueChange = (newValue: string, isSelectEvent = false): void => {
+ let selectedValue: BusinessSearchResult | undefined;
+ if (isSelectEvent) {
+ // Prevent selecting the empty value included when props.contents is empty.
+ selectedValue = newValue
+ ? inputList.value.find((entry) => entry.code === newValue)
+ : undefined;
+ }
emit("update:model-value", selectedValue?.name ?? newValue);
emit("update:selected-value", selectedValue);
@@ -124,7 +125,9 @@ watch(
}
);
watch([inputValue], () => {
- emitValueChange(inputValue.value);
+ if (isUserEvent.value) {
+ emitValueChange(inputValue.value);
+ }
});
/**
@@ -168,7 +171,7 @@ const validateInput = (newValue: string) => {
const selectAutocompleteItem = (event: any) => {
const newValue = event?.detail?.item?.getAttribute("data-id");
- emitValueChange(newValue);
+ emitValueChange(newValue, true);
validateInput(newValue);
};
diff --git a/frontend/src/pages/staffform/FirstNationClientInformationWizardStep.vue b/frontend/src/pages/staffform/FirstNationClientInformationWizardStep.vue
index fd28c9af83..3b8febcb8b 100644
--- a/frontend/src/pages/staffform/FirstNationClientInformationWizardStep.vue
+++ b/frontend/src/pages/staffform/FirstNationClientInformationWizardStep.vue
@@ -75,11 +75,6 @@ watch([autoCompleteResult], () => {
firstNationControl.value = false;
});
-const updateModelValue = ($event) => {
- validation.businessName =
- !!$event && $event === autoCompleteResult.value?.name;
-};
-
const parseSelectedNation = (
selectedNation: FirstNationDetailsDto
) => {
@@ -129,7 +124,7 @@ const mapFirstNationInfo = (firstNations: ForestClientDetailsDto[] = []) => {
enabled
:loading="loading"
@update:selected-value="autoCompleteResult = parseSelectedNation($event)"
- @update:model-value="updateModelValue"
+ @update:model-value="validation.businessName = false"
/>
Loading first nation details...
diff --git a/frontend/tests/unittests/components/forms/AutoCompleteInputComponent.spec.ts b/frontend/tests/unittests/components/forms/AutoCompleteInputComponent.spec.ts
index a432f3f9ce..524238a21f 100644
--- a/frontend/tests/unittests/components/forms/AutoCompleteInputComponent.spec.ts
+++ b/frontend/tests/unittests/components/forms/AutoCompleteInputComponent.spec.ts
@@ -223,7 +223,10 @@ describe("Auto Complete Input Component", () => {
it('emits the "update:selected-value" with undefined when user types something in the input field', async () => {
// adding a 'Z' character to the end of the original value so to trigger an update:model-value
- await wrapper.setProps({ modelValue: "TANGOZ" });
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ wrapper.find(`#${id}`).element._filterInputValue = "TANGOZ";
+ await wrapper.find(`#${id}`).trigger("input");
expect(wrapper.emitted("update:selected-value")).toHaveLength(2);
expect(wrapper.emitted("update:selected-value")![1][0]).toEqual(
@@ -231,6 +234,19 @@ describe("Auto Complete Input Component", () => {
);
});
+ it('doesn\'t emit the "update:selected-value" when the value has been changed from outside', async () => {
+ const expectedCount = 1;
+
+ // Expected count before the update
+ expect(wrapper.emitted("update:selected-value")).toHaveLength(expectedCount);
+
+ // adding a 'Z' character to the end of the original value so to trigger an update:model-value
+ await wrapper.setProps({ modelValue: "TANGOZ" });
+
+ // Expected count after the update
+ expect(wrapper.emitted("update:selected-value")).toHaveLength(expectedCount);
+ });
+
it('emits the "update:selected-value" with the newly selected value', async () => {
await wrapper
.find(`#${id}`)
From 761daafe4c67cb629e4d3f02c24e0a101fd12b27 Mon Sep 17 00:00:00 2001
From: Maria Martinez <77364706+mamartinezmejia@users.noreply.github.com>
Date: Sun, 22 Sep 2024 20:22:11 -0700
Subject: [PATCH 06/28] feat(fe:FSADT1-1510): Load Legal Types description from
data convertor (#1169)
---
...egisteredClientInformationWizardStep.cy.ts | 2 +-
.../fixtures/clients/bcreg_FM123123.json | 4 +-
.../fixtures/clients/bcreg_FM123456.json | 4 +-
.../fixtures/staff/bcregisteredscenarios.json | 22 +++---
frontend/src/dto/ApplyClientNumberDto.ts | 11 +++
frontend/src/helpers/DataConversors.ts | 46 ------------
frontend/src/helpers/DataConverters.ts | 71 +++++++++++++++++++
.../BusinessInformationWizardStep.vue | 6 +-
...cRegisteredClientInformationWizardStep.vue | 22 ++----
...FirstNationClientInformationWizardStep.vue | 4 +-
...versors.spec.ts => DataConverters.spec.ts} | 6 +-
11 files changed, 113 insertions(+), 85 deletions(-)
delete mode 100644 frontend/src/helpers/DataConversors.ts
create mode 100644 frontend/src/helpers/DataConverters.ts
rename frontend/tests/unittests/helpers/{DataConversors.spec.ts => DataConverters.spec.ts} (88%)
diff --git a/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts b/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts
index 7d74e0e201..ca8b73c9c3 100644
--- a/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts
+++ b/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts
@@ -1,7 +1,7 @@
import testCases from "../../../fixtures/staff/bcregisteredscenarios.json";
/* eslint-disable no-undef */
-describe("Bc Registered Staff Wizard Step", () => {
+describe("BC Registered Staff Wizard Step", () => {
beforeEach(() => {
cy.viewport(1920, 1080);
diff --git a/frontend/cypress/fixtures/clients/bcreg_FM123123.json b/frontend/cypress/fixtures/clients/bcreg_FM123123.json
index a01f7fe490..a80ef08f2c 100644
--- a/frontend/cypress/fixtures/clients/bcreg_FM123123.json
+++ b/frontend/cypress/fixtures/clients/bcreg_FM123123.json
@@ -1,5 +1,5 @@
{
- "name": "Sole proprietorship 1",
+ "name": "Sole Proprietorship 1",
"id": "FM123123",
"goodStanding": true,
"addresses": [
@@ -31,4 +31,4 @@
}
],
"isOwnedByPerson": true
-}
\ No newline at end of file
+}
diff --git a/frontend/cypress/fixtures/clients/bcreg_FM123456.json b/frontend/cypress/fixtures/clients/bcreg_FM123456.json
index a01f7fe490..a80ef08f2c 100644
--- a/frontend/cypress/fixtures/clients/bcreg_FM123456.json
+++ b/frontend/cypress/fixtures/clients/bcreg_FM123456.json
@@ -1,5 +1,5 @@
{
- "name": "Sole proprietorship 1",
+ "name": "Sole Proprietorship 1",
"id": "FM123123",
"goodStanding": true,
"addresses": [
@@ -31,4 +31,4 @@
}
],
"isOwnedByPerson": true
-}
\ No newline at end of file
+}
diff --git a/frontend/cypress/fixtures/staff/bcregisteredscenarios.json b/frontend/cypress/fixtures/staff/bcregisteredscenarios.json
index bf5b9a1ef9..3c4b31ae52 100644
--- a/frontend/cypress/fixtures/staff/bcregisteredscenarios.json
+++ b/frontend/cypress/fixtures/staff/bcregisteredscenarios.json
@@ -10,7 +10,7 @@
"showBcRegDownNotification": false,
"showDuplicatedNotification": true,
"showNotOwnedByPersonError": false,
- "type": "Corporation",
+ "type": "Continued In Corporation",
"standing": "Unknown",
"dba": ""
},
@@ -25,7 +25,7 @@
"showBcRegDownNotification": false,
"showDuplicatedNotification": false,
"showNotOwnedByPersonError": false,
- "type": "Sole proprietorship",
+ "type": "Sole Proprietorship",
"standing": "Good standing",
"dba": "Soleprop"
},
@@ -40,7 +40,7 @@
"showBcRegDownNotification": true,
"showDuplicatedNotification": false,
"showNotOwnedByPersonError": false,
- "type": "Corporation",
+ "type": "Continued In Corporation",
"standing": "Good Standing",
"dba": ""
},
@@ -53,12 +53,12 @@
"showUnknowNotification": false,
"showNotGoodStandingNotification": true,
"showBcRegDownNotification": false,
- "type": "Corporation",
+ "type": "Continued In Corporation",
"standing": "Not in good standing",
"dba": ""
},
{
- "scenarioName": "OK State - Corporation",
+ "scenarioName": "OK State - Continued In Corporation",
"companySearch": "cmp",
"companyCode": "C1231231",
"showData": true,
@@ -68,12 +68,12 @@
"showBcRegDownNotification": false,
"showDuplicatedNotification": false,
"showNotOwnedByPersonError": false,
- "type": "Corporation",
+ "type": "Continued In Corporation",
"standing": "Good standing",
"dba": ""
},
{
- "scenarioName": "OK State - Corporation not found",
+ "scenarioName": "OK State - Continued In Corporation not found",
"companySearch": "cnf",
"companyCode": "C9999999",
"showData": true,
@@ -83,7 +83,7 @@
"showBcRegDownNotification": false,
"showDuplicatedNotification": false,
"showNotOwnedByPersonError": false,
- "type": "Corporation",
+ "type": "Continued In Corporation",
"standing": "Unknown",
"dba": ""
},
@@ -111,7 +111,7 @@
"showBcRegDownNotification": false,
"showDuplicatedNotification": false,
"showNotOwnedByPersonError": false,
- "type": "Corporation",
+ "type": "Continued In Corporation",
"standing": "Unknown",
"dba": ""
},
@@ -126,8 +126,8 @@
"showBcRegDownNotification": false,
"showDuplicatedNotification": false,
"showNotOwnedByPersonError": true,
- "type": "Sole proprietorship",
+ "type": "Sole Proprietorship",
"standing": "Good standing",
"dba": "Soleprop"
}
-]
\ No newline at end of file
+]
diff --git a/frontend/src/dto/ApplyClientNumberDto.ts b/frontend/src/dto/ApplyClientNumberDto.ts
index 1e92cd7de7..84113d7290 100644
--- a/frontend/src/dto/ApplyClientNumberDto.ts
+++ b/frontend/src/dto/ApplyClientNumberDto.ts
@@ -1,4 +1,5 @@
import type { CodeDescrType, IdentificationCodeDescrType } from "@/dto/CommonTypesDto";
+import { toSentenceCase } from "@/services/ForestClientService";
export interface Address {
streetAddress: string;
@@ -96,6 +97,16 @@ export const indexedEmptyAddress = (index: number): Address =>
export const emptyAddress = (): Address => indexedEmptyAddress(0);
+export const formatAddresses = (addresses: Address[]): Address[] => {
+ if (addresses && addresses.length > 0) {
+ return addresses.map(address => ({
+ ...address,
+ locationName: toSentenceCase(address.locationName),
+ }));
+ }
+ return [{ ...emptyAddress(), locationName: locationName.text }];
+};
+
export const indexedEmptyContact = (index: number): Contact =>
JSON.parse(
JSON.stringify({
diff --git a/frontend/src/helpers/DataConversors.ts b/frontend/src/helpers/DataConversors.ts
deleted file mode 100644
index 08fd541753..0000000000
--- a/frontend/src/helpers/DataConversors.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { type Address, emptyAddress, locationName } from "@/dto/ApplyClientNumberDto";
-import { toSentenceCase } from "@/services/ForestClientService";
-
-export const retrieveClientType = (legalType: string): string => {
- if (legalType) {
- switch (legalType) {
- case "A":
- case "B":
- case "BC":
- case "C":
- case "CP":
- case "EPR":
- case "FOR":
- case "LIC":
- case "REG":
- return "C";
- case "S":
- case "XS":
- return "S";
- case "XCP":
- return "A";
- case "SP":
- return "RSP";
- case "GP":
- return "P";
- case "LP":
- case "XL":
- case "XP":
- return "L";
- default:
- throw new Error("Unknown Legal Type " + legalType);
- }
- } else {
- return "";
- }
-};
-
-export const exportAddress = (addresses: Address[]): Address[] => {
- if (addresses && addresses.length > 0) {
- return addresses.map(address => ({
- ...address,
- locationName: toSentenceCase(address.locationName),
- }));
- }
- return [{ ...emptyAddress(), locationName: locationName.text }];
-};
diff --git a/frontend/src/helpers/DataConverters.ts b/frontend/src/helpers/DataConverters.ts
new file mode 100644
index 0000000000..a69bd5dff2
--- /dev/null
+++ b/frontend/src/helpers/DataConverters.ts
@@ -0,0 +1,71 @@
+export const retrieveClientType = (legalType: string): string => {
+ if (legalType) {
+ switch (legalType) {
+ case "A":
+ case "B":
+ case "BC":
+ case "C":
+ case "CP":
+ case "EPR":
+ case "FOR":
+ case "LIC":
+ case "REG":
+ return "C";
+ case "S":
+ case "XS":
+ return "S";
+ case "XCP":
+ return "A";
+ case "SP":
+ return "RSP";
+ case "GP":
+ return "P";
+ case "LP":
+ case "XL":
+ case "XP":
+ return "L";
+ default:
+ throw new Error("Unsupported Legal Type " + legalType);
+ }
+ } else {
+ return "";
+ }
+};
+
+export const retrieveLegalTypeDesc = (legalType: string): string => {
+ switch (legalType) {
+ case "A":
+ case "B":
+ case "EPR":
+ case "REG":
+ return "Extraprovincial Company ";
+ case "BC":
+ return "BC Company";
+ case "C":
+ return "Continued In Corporation";
+ case "CP":
+ return "Cooperative";
+ case "FOR":
+ return "Foreign Registration";
+ case "GP":
+ return "General Partnership";
+ case "LIC":
+ return "Licensed (Extra-Pro)";
+ case "LP":
+ return "Limited Partnership";
+ case "S":
+ return "Society";
+ case "SP":
+ return "Sole Proprietorship";
+ case "XS":
+ return "Extraprovincial Society";
+ case "XCP":
+ return "Extraprovincial Cooperative";
+ case "XL":
+ return "Extraprovincial Limited Liability Partnership";
+ case "XP":
+ return "Extraprovincial Limited Partnership";
+ default:
+ return legalType + " (Unknown)";
+ }
+};
diff --git a/frontend/src/pages/bceidform/BusinessInformationWizardStep.vue b/frontend/src/pages/bceidform/BusinessInformationWizardStep.vue
index 149db69e2e..b7110b9007 100644
--- a/frontend/src/pages/bceidform/BusinessInformationWizardStep.vue
+++ b/frontend/src/pages/bceidform/BusinessInformationWizardStep.vue
@@ -15,7 +15,7 @@ import {
ProgressNotification,
} from "@/dto/CommonTypesDto";
import { BusinessTypeEnum, CodeNameType } from "@/dto/CommonTypesDto";
-import { locationName as defaultLocation } from "@/dto/ApplyClientNumberDto";
+import { locationName as defaultLocation, formatAddresses } from "@/dto/ApplyClientNumberDto";
import type {
FormDataDto,
ForestClientDetailsDto,
@@ -24,7 +24,7 @@ import type {
import { getValidations } from "@/helpers/validators/GlobalValidators";
import { submissionValidation } from "@/helpers/validators/SubmissionValidators";
// Importing helper functions
-import { retrieveClientType, exportAddress } from "@/helpers/DataConversors";
+import { retrieveClientType } from "@/helpers/DataConverters";
import {
getEnumKeyByEnumValue,
adminEmail,
@@ -285,7 +285,7 @@ watch([detailsData], () => {
...forestClientDetails.contacts,
];
- formData.value.location.addresses = exportAddress(
+ formData.value.location.addresses = formatAddresses(
forestClientDetails.addresses
);
diff --git a/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue b/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue
index c35d5c2296..04bc3ef6f5 100644
--- a/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue
+++ b/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue
@@ -23,7 +23,8 @@ import {
import { getEnumKeyByEnumValue } from "@/services/ForestClientService";
import { BusinessTypeEnum } from "@/dto/CommonTypesDto";
// Importing helper functions
-import { retrieveClientType, exportAddress } from "@/helpers/DataConversors";
+import { retrieveClientType, retrieveLegalTypeDesc } from "@/helpers/DataConverters";
+import { formatAddresses } from "@/dto/ApplyClientNumberDto";
// Importing validators
import { getValidations } from "@/helpers/validators/StaffFormValidations";
import { submissionValidation } from "@/helpers/validators/SubmissionValidators";
@@ -92,19 +93,10 @@ const standingMessage = computed(() => {
return "Unknown";
});
-//TODO: Either load from BE or add to DataConversors.ts
-const legalTypeText = computed(() => {
- if (formData.value.businessInformation.legalType === "C") {
- return "Corporation";
- }
- if (formData.value.businessInformation.legalType === "SP") {
- return "Sole proprietorship";
- }
- if (formData.value.businessInformation.legalType === "GP") {
- return "General Partnership";
- }
- return formData.value.businessInformation.legalType + " (Unknown)";
-});
+//We're using DataConverters.ts for this since it's not being saved to the database, so creating a table isn't necessary.
+const legalTypeText = computed(() =>
+ retrieveLegalTypeDesc(formData.value.businessInformation.legalType)
+);
const autoCompleteUrl = computed(
() =>
@@ -280,7 +272,7 @@ watch([detailsData], () => {
address.emailAddress = null;
address.notes = null;
});
- formData.value.location.addresses = exportAddress(receivedAddresses);
+ formData.value.location.addresses = formatAddresses(receivedAddresses);
formData.value.businessInformation.goodStandingInd = standingValue(
forestClientDetails.goodStanding
diff --git a/frontend/src/pages/staffform/FirstNationClientInformationWizardStep.vue b/frontend/src/pages/staffform/FirstNationClientInformationWizardStep.vue
index 3b8febcb8b..60de11074c 100644
--- a/frontend/src/pages/staffform/FirstNationClientInformationWizardStep.vue
+++ b/frontend/src/pages/staffform/FirstNationClientInformationWizardStep.vue
@@ -10,7 +10,7 @@ import type {
FormDataDto,
} from "@/dto/ApplyClientNumberDto";
import type { BusinessSearchResult } from "@/dto/CommonTypesDto";
-import { exportAddress } from "@/helpers/DataConversors";
+import { formatAddresses } from "@/dto/ApplyClientNumberDto";
import { getValidations } from "@/helpers/validators/StaffFormValidations";
import { submissionValidation } from "@/helpers/validators/SubmissionValidators";
@@ -80,7 +80,7 @@ const parseSelectedNation = (
) => {
if (selectedNation) {
validation.businessName = true;
- formData.value.location.addresses = exportAddress(
+ formData.value.location.addresses = formatAddresses(
selectedNation.addresses
);
formData.value.businessInformation.registrationNumber = `DINA${selectedNation.id}`;
diff --git a/frontend/tests/unittests/helpers/DataConversors.spec.ts b/frontend/tests/unittests/helpers/DataConverters.spec.ts
similarity index 88%
rename from frontend/tests/unittests/helpers/DataConversors.spec.ts
rename to frontend/tests/unittests/helpers/DataConverters.spec.ts
index bfd76cacf5..42b5e2e657 100644
--- a/frontend/tests/unittests/helpers/DataConversors.spec.ts
+++ b/frontend/tests/unittests/helpers/DataConverters.spec.ts
@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest'
-import { retrieveClientType } from '@/helpers/DataConversors'
+import { retrieveClientType } from '@/helpers/DataConverters'
-describe('DataConversors.ts', () => {
+describe('DataConverters.ts', () => {
it('retrieveClientType should return C when legalType is A', () => {
const result = retrieveClientType('A')
expect(result).toEqual('C')
@@ -29,6 +29,6 @@ describe('DataConversors.ts', () => {
it('throw an error when legalType is unknown', () => {
expect(() => {
retrieveClientType('Z')
- }).toThrow('Unknown Legal Type Z')
+ }).toThrow('Unsupported Legal Type Z')
})
})
From 9b7be0ebc2e0d1a52cf4992ee2760b9d5049c959 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 23 Sep 2024 03:23:17 +0000
Subject: [PATCH 07/28] fix(deps): update dependency date-fns to v4
---
frontend/package-lock.json | 8 ++++----
frontend/package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9ec38a2aa3..98a19a144b 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -18,7 +18,7 @@
"@vueuse/core": "^11.1.0",
"aws-amplify": "^6.3.5",
"axios": "^1.7.2",
- "date-fns": "^3.0.0",
+ "date-fns": "^4.0.0",
"dotenv": "^16.0.0",
"vue": "^3.5.6",
"vue-dompurify-html": "^5.0.1",
@@ -9214,9 +9214,9 @@
}
},
"node_modules/date-fns": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
- "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
diff --git a/frontend/package.json b/frontend/package.json
index e2fc886de6..d4c6845afd 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -56,7 +56,7 @@
"@vueuse/core": "^11.1.0",
"aws-amplify": "^6.3.5",
"axios": "^1.7.2",
- "date-fns": "^3.0.0",
+ "date-fns": "^4.0.0",
"dotenv": "^16.0.0",
"vue": "^3.5.6",
"vue-dompurify-html": "^5.0.1",
From d4dd307ace927cc97d02abb1f980b1f0f5379d75 Mon Sep 17 00:00:00 2001
From: Fernando Terra <79578735+fterra-encora@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:31:35 -0300
Subject: [PATCH 08/28] fix(fe: FSADT1-1495): Add notification to all BC
Registries cards in step 1 (#1171)
fix: show the Read-only notification always
---
...egisteredClientInformationWizardStep.cy.ts | 24 +++++++++----------
...cRegisteredClientInformationWizardStep.vue | 1 -
2 files changed, 12 insertions(+), 13 deletions(-)
diff --git a/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts b/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts
index ca8b73c9c3..4ba419e204 100644
--- a/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts
+++ b/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts
@@ -445,21 +445,21 @@ describe("BC Registered Staff Wizard Step", () => {
}
if (scenario.showData) {
- const success = Object.entries(scenario)
- .filter(
- ([key, value]) =>
- key.startsWith("show") && key.endsWith("Notification")
- )
- .map(([key, value]) => value)
- .every((value) => value === false);
+ /*
+ This variable might be useful in the future to test the button Next gets enabled on
+ success. But we'll probably need to fix FSADT1-1496 first.
+ */
+ // const success = Object.entries(scenario)
+ // .filter(
+ // ([key, value]) =>
+ // key.startsWith("show") && key.endsWith("Notification")
+ // )
+ // .map(([key, value]) => value)
+ // .every((value) => value === false);
cy.get(
".read-only-box > cds-inline-notification#readOnlyNotification"
- ).should(
- success || scenario.showDuplicatedNotification
- ? "exist"
- : "not.exist"
- );
+ ).should("exist");
cy.get(`.read-only-box > #legalType > .title-group-01 > .label-01`)
.should("exist")
diff --git a/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue b/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue
index 04bc3ef6f5..44db8686f3 100644
--- a/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue
+++ b/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue
@@ -390,7 +390,6 @@ onMounted(() => {
Date: Tue, 24 Sep 2024 18:04:06 -0300
Subject: [PATCH 09/28] chore: update support/client2 with main (#1175)
* fix(deps): update maven all non-major dependencies
* fix(deps): update dependency date-fns to v4 (#1173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
From 1a3117e03409132b36c7cd47c189e2015321b23c Mon Sep 17 00:00:00 2001
From: Fernando Terra <79578735+fterra-encora@users.noreply.github.com>
Date: Wed, 25 Sep 2024 09:43:57 -0300
Subject: [PATCH 10/28] fix(fe: FSADT1-1511): Clear business information when a
new Client name gets selected (#1176)
fix: clear business information when a new client name gets selected
---
...egisteredClientInformationWizardStep.cy.ts | 51 +++++++++++++++++++
...cRegisteredClientInformationWizardStep.vue | 9 +++-
2 files changed, 59 insertions(+), 1 deletion(-)
diff --git a/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts b/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts
index 4ba419e204..f070fea051 100644
--- a/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts
+++ b/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts
@@ -629,6 +629,57 @@ describe("BC Registered Staff Wizard Step", () => {
});
});
+ // See FSADT1-1511 (https://apps.nrs.gov.bc.ca/int/jira/browse/FSADT1-1511)
+ describe("when the selected Client name is replaced from a Sole proprietorship to something else", () => {
+ it("clears the Doing Business As field", () => {
+ loginAndNavigateToStaffForm();
+
+ cy.get("cds-inline-notification#bcRegistrySearchNotification").should(
+ "exist"
+ );
+
+ const sppSearch = "spp";
+ const sppCode = "FM123123";
+
+ interceptClientsApi(sppSearch, sppCode);
+
+ cy.selectAutocompleteEntry(
+ "#businessName",
+ sppSearch,
+ sppCode,
+ `@clientSearch${sppSearch}`
+ );
+
+ cy.get(`.read-only-box > #legalType`)
+ .should("exist")
+ .and("contains.text", "Sole Proprietorship");
+
+ cy.clearFormEntry("#businessName");
+
+ const cmpSearch = "cmp";
+ const cmpCode = "C1231231";
+
+ interceptClientsApi(cmpSearch, cmpCode);
+
+ cy.selectAutocompleteEntry(
+ "#businessName",
+ cmpSearch,
+ cmpCode,
+ `@clientSearch${cmpSearch}`
+ );
+
+ cy.get(`.read-only-box > #legalType`)
+ .should("exist")
+ .and("contains.text", "Continued In Corporation");
+
+ /*
+ Doing Business As is cleared, instead of holding the name of the previously selected Sole
+ proprietorship.
+ */
+ cy.get("#doingBusinessAs").should("exist").and("have.value", "");
+ });
+ });
+
const loginAndNavigateToStaffForm = () => {
cy.visit("/");
diff --git a/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue b/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue
index 44db8686f3..2dce84b1f8 100644
--- a/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue
+++ b/frontend/src/pages/staffform/BcRegisteredClientInformationWizardStep.vue
@@ -18,7 +18,8 @@ import {
type FormDataDto,
type ForestClientDetailsDto,
indexedEmptyAddress,
- indexedEmptyContact
+ indexedEmptyContact,
+ newFormDataDto,
} from "@/dto/ApplyClientNumberDto";
import { getEnumKeyByEnumValue } from "@/services/ForestClientService";
import { BusinessTypeEnum } from "@/dto/CommonTypesDto";
@@ -117,6 +118,12 @@ watch([autoCompleteResult], () => {
bcRegistryError.value = false;
showOnError.value = false;
+ // reset businessInformation
+ Object.assign(formData.value.businessInformation, {
+ ...newFormDataDto().businessInformation,
+ businessName: formData.value.businessInformation.businessName,
+ });
+
if (autoCompleteResult.value && autoCompleteResult.value.code) {
formData.value.businessInformation.registrationNumber = autoCompleteResult.value.code;
formData.value.businessInformation.legalType = autoCompleteResult.value.legalType;
From 612044557862b0322c7ef5137f58ea6204b6f38c Mon Sep 17 00:00:00 2001
From: Paulo Gomes da Cruz Junior
Date: Wed, 25 Sep 2024 15:18:45 -0700
Subject: [PATCH 11/28] feat(FSADT1-779): adding cypress scenarios (#1139)
* feat: adding some cypress code
* feat: adding gherking issue template
* feat: updated commands and feature files
* feat: adding core cypress instructions for user flow
* chore: testing env var
* chore: testing artifact grab
* chore: ci update
* chore: debugging
* chore: reducing verbosity
* chore: removing test data
* feat: adding initial sample for creation
* docs: updated documentation
* feat: clean before run
This will make the database remove the test data beforehand. This can affect the manual test too, but is the price for now
* fix: fixing clean script
* chore: changing strategy for cleanup
- Changed strategy for cleanup. First remove everything from the PR database before recreating it. This is required due to tue uncertainty of the data that will be created through cypress and the previous, manual data inserted as well
* chore: testing a few new steps
* chore: pre-tools cleanup
* fix: fixing automation test
* fix: fixing staff on automation
* chore: changing wait time
* chore: adding an address
* chore: trying to make a miracle
* chore: scale down
* chore: changing config
* ci: lets do it
* chore: some tests
* ci: scale down post work and scale up
* chore: adding tags to make it easier to find component
* chore: updating name part
* chore: updating a little bit
* chore: adding bceid and bcsc button text
* feat: updated login hooks
* feat: add tag identifier for internal and external user
* feat: added initial bceid
* chore: adding contact identification for old screens
* chore: changed limits for submission
* chore: adding identification to notification
* fix: fixing submit instruction
* chore: splitting up the instructions
* fix: fixed bceid
* docs: updated readme for cypress
* chore: updating notification identification
* fix: fixing automation
* feat(FSADT1-779): updating gherkin issue template
* feat: added action to generate feature files
* docs: updated readme
* chore: reverting a change on accordion item title
* fix: fixing wrapper
* chore: adding decorator config
* fix: fixing data-text location
* chore: changing data-text location
* fix: changing data-text location
* fix: fixing contact selector
* chore: removing extra data-text
* chore: fixing subgroup search
---
.github/ISSUE_TEMPLATE/cypress.yml | 65 +++++
.github/workflows/issue-gherkin.yml | 49 ++++
.github/workflows/pr-open.yml | 223 +++++++++++++++++-
backend/openshift.configmap.dev.yml | 6 +
cypress/README.md | 168 ++++++++++++-
cypress/cypress.config.ts | 10 +
cypress/cypress/e2e/bceid.feature | 67 ++++++
cypress/cypress/e2e/bceid.ts | 2 +
cypress/cypress/e2e/landing.feature | 27 +++
cypress/cypress/e2e/landing.ts | 2 +
cypress/cypress/e2e/logouts.feature | 9 +
cypress/cypress/e2e/sample.feature | 4 +-
cypress/cypress/e2e/sample.ts | 11 +-
cypress/cypress/e2e/staffCreate.feature | 95 ++++++++
cypress/cypress/e2e/staffCreate.ts | 2 +
cypress/cypress/support/commands.ts | 107 +++------
cypress/cypress/support/cypress.d.ts | 12 +-
cypress/cypress/support/e2e.ts | 13 +
.../step_definitions/area_input_steps.ts | 44 ++++
.../step_definitions/autocomplete_steps.ts | 62 +++++
.../support/step_definitions/button_steps.ts | 74 ++++++
.../support/step_definitions/commons.ts | 74 ++++++
.../support/step_definitions/general_steps.ts | 35 +++
.../support/step_definitions/loginsHooks.ts | 71 ++++++
.../step_definitions/radio_check_steps.ts | 18 ++
.../step_definitions/select_input_steps.ts | 60 +++++
.../support/step_definitions/tables_steps.ts | 63 +++++
.../step_definitions/text_input_steps.ts | 77 ++++++
.../support/step_definitions/ui_ux_steps.ts | 97 ++++++++
.../grouping/ContactGroupComponent.vue | 10 +-
frontend/src/pages/FormBCSCPage.vue | 3 +
frontend/src/pages/FormBCeIDPage.vue | 5 +
frontend/src/pages/FormStaffPage.vue | 4 +
.../BusinessInformationWizardStep.vue | 5 +
...cRegisteredClientInformationWizardStep.vue | 5 +
.../pages/staffform/ContactsWizardStep.vue | 6 +-
.../pages/staffform/LocationsWizardStep.vue | 6 +-
frontend/tsconfig.base.json | 2 +
legacydb/Dockerfile | 3 +
legacydb/cleanDatabase | 26 ++
40 files changed, 1514 insertions(+), 108 deletions(-)
create mode 100644 .github/ISSUE_TEMPLATE/cypress.yml
create mode 100644 .github/workflows/issue-gherkin.yml
create mode 100644 cypress/cypress/e2e/bceid.feature
create mode 100644 cypress/cypress/e2e/bceid.ts
create mode 100644 cypress/cypress/e2e/landing.feature
create mode 100644 cypress/cypress/e2e/landing.ts
create mode 100644 cypress/cypress/e2e/logouts.feature
create mode 100644 cypress/cypress/e2e/staffCreate.feature
create mode 100644 cypress/cypress/e2e/staffCreate.ts
create mode 100644 cypress/cypress/support/step_definitions/area_input_steps.ts
create mode 100644 cypress/cypress/support/step_definitions/autocomplete_steps.ts
create mode 100644 cypress/cypress/support/step_definitions/button_steps.ts
create mode 100644 cypress/cypress/support/step_definitions/commons.ts
create mode 100644 cypress/cypress/support/step_definitions/general_steps.ts
create mode 100644 cypress/cypress/support/step_definitions/loginsHooks.ts
create mode 100644 cypress/cypress/support/step_definitions/radio_check_steps.ts
create mode 100644 cypress/cypress/support/step_definitions/select_input_steps.ts
create mode 100644 cypress/cypress/support/step_definitions/tables_steps.ts
create mode 100644 cypress/cypress/support/step_definitions/text_input_steps.ts
create mode 100644 cypress/cypress/support/step_definitions/ui_ux_steps.ts
create mode 100644 legacydb/cleanDatabase
diff --git a/.github/ISSUE_TEMPLATE/cypress.yml b/.github/ISSUE_TEMPLATE/cypress.yml
new file mode 100644
index 0000000000..5ee6bb522e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/cypress.yml
@@ -0,0 +1,65 @@
+name: User provided automated tests
+description: Submit Gherkin scenarios to generate .feature files for our test suite.
+title: '[Test Case]: REPLACE WITH YOUR TEST CASE TITLE'
+labels: gherkin, automated test
+body:
+ - type: markdown
+ attributes:
+ value: |
+ ## 📝 Gherkin Scenario Submission
+
+ Thank you for contributing! Please provide your Gherkin scenario in the format below.
+
+ ### Gherkin/Cucumber Test Scenario
+ - ✍️ **Write or paste your Gherkin scenario in the first block**:
+ - Ensure the scenario follows the structure of **Given**, **When**, **Then**.
+ - Ensure the scenario contains a **Feature** and **Scenario** instruction.
+ - You can have multiple **Scenarios** in a single **Feature**.
+ - Pay attention to the **indentation** and **keywords**, as misconfigured files will cause issues when executed.
+ - Pay attention to the data you're using in your scenario. Make sure it don't pose a security risk or exposes any **REAL** [personal identifiable information](https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/privacy/personal-information).
+ - If you're unsure how to format your scenario, [click here for a guide](https://github.com/bcgov/nr-forest-client/tree/main/cypress#readme).
+ - 🧩 **Additional instructions**:
+ - Use the second block to provide any **non-default** steps or custom behavior that doesn’t fit into our predefined instructions. You can find a list of ready-made instructions in [our documentation](https://github.com/bcgov/nr-forest-client/tree/main/cypress#readme).
+
+ ### Example
+
+ ```gherkin
+ Feature: Guess the word
+
+ # The first example has two steps
+ Scenario: Maker starts a game
+ When the Maker starts a game
+ Then the Maker waits for a Breaker to join
+
+ # The second example has three steps
+ Scenario: Breaker joins a game
+ Given the Maker has started a game with the word "silky"
+ When the Breaker joins the Maker's game
+ Then the Breaker must guess a word with 5 characters
+ ```
+ - type: textarea
+ id: gherkin_scenario
+ attributes:
+ label: Test Scenario
+ description: 'Please write or paste your Gherkin scenario here.'
+ render: gherkin
+ placeholder: |
+ Feature: [Feature Name]
+ Scenario: [Scenario Name]
+ Given [some initial context]
+ When [some event occurs]
+ Then [some outcome should occur]
+ validations:
+ required: true
+
+ - type: textarea
+ id: additional_instructions
+ attributes:
+ label: Additional Instructions
+ description: 'Please provide any additional instructions or custom behavior that doesn’t fit into our predefined instructions.'
+ render: shell
+ placeholder: |
+ - [Instruction 1] - [Description]
+ - [I can choose "rock" as the word] - User can provide a custom word to guess.
+ validations:
+ required: false
diff --git a/.github/workflows/issue-gherkin.yml b/.github/workflows/issue-gherkin.yml
new file mode 100644
index 0000000000..49a2069a05
--- /dev/null
+++ b/.github/workflows/issue-gherkin.yml
@@ -0,0 +1,49 @@
+name: Create Gherkin Feature from Issue
+on:
+ issues:
+ types:
+ - opened
+
+jobs:
+ create-feature:
+ if: contains(github.event.issue.labels.*.name, 'gherkin')
+ runs-on: ubuntu-22.04
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ fetch-depth: 0
+
+ - name: Extract feature content
+ id: feature
+ uses: bcgov-nr/action-gherkin-issue-processor@v0.0.2
+ with:
+ issue: ${{ github.event.issue.number }}
+ default_title: "[Test Case]: REPLACE WITH YOUR TEST CASE TITLE"
+ update_title: true
+
+ - name: Create feature file
+ run: echo "${{ steps.feature.outputs.feature }}" > cypress/cypress/e2e/upt_${{ github.event.issue.number }}.feature
+
+ - name: Commit & Push changes
+ uses: Andro999b/push@v1.3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ branch: test/upt_${{ github.event.issue.number }}
+ force: true
+ message: |
+ test(upt #${{ github.event.issue.number }}): ${{ steps.feature.outputs.title }}
+
+ Closes #${{ github.event.issue.number }}
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v5
+ with:
+ branch: test/upt_${{ github.event.issue.number }}
+ title: "test(upt #${{ github.event.issue.number }}): ${{ steps.feature.outputs.title }}"
+ body: |
+ ${{ github.event.issue.body }}
+
+ Closes #${{ github.event.issue.number }}
diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml
index 8a5801ee7f..bcac84e694 100644
--- a/.github/workflows/pr-open.yml
+++ b/.github/workflows/pr-open.yml
@@ -82,9 +82,36 @@ jobs:
build_args: |
APP_VERSION=${{ needs.vars.outputs.semver }}-${{ github.event.number }}
+ pre-tools:
+ name: Pre Deploy Tools
+ needs: [build-legacydb, vars]
+ environment: dev
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - name: Scale down legacy
+ continue-on-error: true
+ run: |
+ oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }}
+ oc project ${{ secrets.OC_NAMESPACE }} # Safeguard!
+ oc scale dc/nr-forest-client-${{ github.event.number }}-legacy --replicas=0 -n ${{ secrets.OC_NAMESPACE }}
+ undesired_replicas=0
+
+ while true; do
+ available_replicas=$(oc get dc/nr-forest-client-${{ github.event.number }}-legacy -n ${{ secrets.OC_NAMESPACE }} -o jsonpath='{.status.availableReplicas}')
+
+ if [[ "$available_replicas" -ge "$undesired_replicas" ]]; then
+ echo "DeploymentConfig ${{ secrets.OC_NAMESPACE }}-${{ github.event.number }}-legacy is now available with $available_replicas replicas."
+ break
+ fi
+
+ echo "Waiting... ($available_replicas pods available)"
+ sleep 5
+ done
+
deploy-tools:
name: Deploy Tools
- needs: [build-legacydb, vars]
+ needs: [pre-tools, build-legacydb, vars]
environment: tools
env:
DOMAIN: apps.silver.devops.gov.bc.ca
@@ -107,6 +134,28 @@ jobs:
-p ORACLEDB_PASSWORD_W=${{ secrets.ORACLEDB_PASSWORD_W }}
-p TAG=latest
+ - name: Remove the PR database
+ continue-on-error: true
+ run: |
+ oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }}
+ oc project ${{ secrets.OC_NAMESPACE }} # Safeguard!
+ # This removes a new pluggable database, user and service for the PR
+ for i in {1..5}; do
+ POD_NAME=$(oc get pods -l app=nr-forest-client-tools -l deployment=nr-forest-client-tools-legacydb -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
+ if [ -n "$POD_NAME" ]; then
+ echo "Pod found: $POD_NAME"
+ oc exec $POD_NAME -- /opt/oracle/removeDatabase "THE" "PR_${{ github.event.number }}"
+ break
+ else
+ echo "Pod not found, retrying in 10 seconds... ($i/5)"
+ sleep 10
+ fi
+ done
+
+ if [ -z "$POD_NAME" ]; then
+ echo "Failed to find the pod after 5 attempts."
+ fi
+
- name: Create the PR database
continue-on-error: true
run: |
@@ -160,7 +209,7 @@ jobs:
oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }}
oc project ${{ secrets.OC_NAMESPACE }} # Safeguard!
oc create job --from=cronjob/nr-forest-client-tools-migratedb migrate-pr${{ github.event.number }}-${{ github.run_attempt }}-$(date +%s) --dry-run=client -o yaml | sed "s/value: main/value: ${ESCAPED_BRANCH_NAME}/" | sed "s/value: \"0\"/value: \"${{ github.event.number }}\"/" | oc apply -f -
-
+
deploy:
name: Deploy Application
needs: [deploy-tools, builds, vars]
@@ -339,7 +388,7 @@ jobs:
name: "User flow test"
runs-on: ubuntu-22.04
needs: [deploy, vars]
- environment: dev
+ environment: tools
env:
URL: ${{ needs.vars.outputs.url }}
steps:
@@ -358,6 +407,16 @@ jobs:
env:
CYPRESS_baseUrl: https://${{ env.URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CYPRESS_editor_password: ${{ secrets.UAT_EDITOR_PASSWORD }}
+ CYPRESS_editor_username: ${{ secrets.UAT_EDITOR_USERNAME }}
+ CYPRESS_admin_password: ${{ secrets.UAT_ADMIN_PASSWORD }}
+ CYPRESS_admin_username: ${{ secrets.UAT_ADMIN_USERNAME }}
+ CYPRESS_viewer_password: ${{ secrets.UAT_VIEWER_PASSWORD }}
+ CYPRESS_viewer_username: ${{ secrets.UAT_VIEWER_USERNAME }}
+ CYPRESS_bceid_password: ${{ secrets.UAT_BCEID_PASSWORD }}
+ CYPRESS_bceid_username: ${{ secrets.UAT_BCEID_USERNAME }}
+ CYPRESS_bcsc_password: ${{ secrets.UAT_BCSC_PASSWORD }}
+ CYPRESS_bcsc_username: ${{ secrets.UAT_BCSC_USERNAME }}
- name: Publish Cypress Results
uses: mikepenz/action-junit-report@v4
@@ -370,18 +429,168 @@ jobs:
detailed_summary: true
job_name: User Journeys
+ - name: Check for Cypress Screenshots and Videos
+ run: |
+ if [ -d "cypress/cypress/screenshots" ] && [ "$(ls -A cypress/cypress/screenshots)" ]; then
+ echo "Screenshots folder is not empty, uploading artifacts."
+ echo "screenshots=true" >> $GITHUB_OUTPUT
+
+ else
+ echo "Screenshots folder is empty or does not exist."
+ echo "screenshots=false" >> $GITHUB_OUTPUT
+ fi
+
+ if [ -d "cypress/cypress/videos" ] && [ "$(ls -A cypress/cypress/videos)" ]; then
+ echo "Videos folder is not empty, uploading artifacts."
+ echo "videos=true" >> $GITHUB_OUTPUT
+
+ else
+ echo "Videos folder is empty or does not exist."
+ echo "videos=false" >> $GITHUB_OUTPUT
+ fi
+ id: check_artifacts
+
- uses: actions/upload-artifact@v4
- name: Upload Cypress Screenshots with error
- if: failure()
+ name: Upload Cypress Screenshots
+ if: always()
with:
name: cypress-screenshots
path: cypress/cypress/screenshots
retention-days: 7
- uses: actions/upload-artifact@v4
- name: Upload Cypress Videos with error
- if: failure()
+ name: Upload Cypress Videos
+ if: always()
with:
name: cypress-videos
path: cypress/cypress/videos
retention-days: 7
+
+ scale-down-after:
+ name: Scale down legacy
+ needs: [cypress-run]
+ environment: dev
+ if: always()
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Stop the Legacy Service
+ continue-on-error: true
+ run: |
+ oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }}
+ oc project ${{ secrets.OC_NAMESPACE }} # Safeguard!
+ oc scale dc/nr-forest-client-${{ github.event.number }}-legacy --replicas=0
+ undesired_replicas=0
+ while true; do
+ available_replicas=$(oc get dc/nr-forest-client-${{ github.event.number }}-legacy -n ${{ secrets.OC_NAMESPACE }} -o jsonpath='{.status.availableReplicas}')
+
+ if [[ "$available_replicas" -ge "$undesired_replicas" ]]; then
+ echo "DeploymentConfig ${{ secrets.OC_NAMESPACE }}-${{ github.event.number }}-legacy is now available with $available_replicas replicas."
+ break
+ fi
+
+ echo "Waiting... ($available_replicas pods available)"
+ sleep 5
+ done
+
+ recreate-database:
+ name: Recreate database
+ needs: [scale-down-after]
+ environment: tools
+ if: always()
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Remove the PR database
+ continue-on-error: true
+ run: |
+ oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }}
+ oc project ${{ secrets.OC_NAMESPACE }} # Safeguard!
+ # This removes a new pluggable database, user and service for the PR
+ for i in {1..5}; do
+ POD_NAME=$(oc get pods -l app=nr-forest-client-tools -l deployment=nr-forest-client-tools-legacydb -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
+ if [ -n "$POD_NAME" ]; then
+ echo "Pod found: $POD_NAME"
+ oc exec $POD_NAME -- /opt/oracle/removeDatabase "THE" "PR_${{ github.event.number }}"
+ break
+ else
+ echo "Pod not found, retrying in 10 seconds... ($i/5)"
+ sleep 10
+ fi
+ done
+
+ if [ -z "$POD_NAME" ]; then
+ echo "Failed to find the pod after 5 attempts."
+ fi
+
+ - name: Create the PR database
+ continue-on-error: true
+ run: |
+ oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }}
+ oc project ${{ secrets.OC_NAMESPACE }} # Safeguard!
+ # This creates a new pluggable database for the PR
+ for i in {1..5}; do
+ POD_NAME=$(oc get pods -l app=nr-forest-client-tools -l deployment=nr-forest-client-tools-legacydb -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
+ if [ -n "$POD_NAME" ]; then
+ echo "Pod found: $POD_NAME"
+ oc exec $POD_NAME -- /opt/oracle/createDatabase PR_${{ github.event.number }}
+ break
+ else
+ echo "Pod not found, retrying in 10 seconds... ($i/5)"
+ sleep 10
+ fi
+ done
+
+ if [ -z "$POD_NAME" ]; then
+ echo "Failed to find the pod after 5 attempts."
+ fi
+
+ - name: Create the PR user
+ continue-on-error: true
+ run: |
+ oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }}
+ oc project ${{ secrets.OC_NAMESPACE }} # Safeguard!
+ # This creates a new pluggable database for the PR
+ for i in {1..5}; do
+ POD_NAME=$(oc get pods -l app=nr-forest-client-tools -l deployment=nr-forest-client-tools-legacydb -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
+ if [ -n "$POD_NAME" ]; then
+ echo "Pod found: $POD_NAME"
+ oc exec $POD_NAME -- /opt/oracle/createAppUser "THE" "${{ secrets.ORACLEDB_PASSWORD_W }}_${{ github.event.number }}" "PR_${{ github.event.number }}"
+ break
+ else
+ echo "Pod not found, retrying in 10 seconds... ($i/5)"
+ sleep 10
+ fi
+ done
+
+ if [ -z "$POD_NAME" ]; then
+ echo "Failed to find the pod after 5 attempts."
+ fi
+
+ - name: Migrate the PR database
+ continue-on-error: true
+ run: |
+ BRANCH_NAME="${{ github.head_ref }}"
+ # Escape slashes and other special characters
+ ESCAPED_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[\/&]/\\&/g')
+ oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }}
+ oc project ${{ secrets.OC_NAMESPACE }} # Safeguard!
+ oc create job --from=cronjob/nr-forest-client-tools-migratedb migrate-pr${{ github.event.number }}-${{ github.run_attempt }}-$(date +%s) --dry-run=client -o yaml | sed "s/value: main/value: ${ESCAPED_BRANCH_NAME}/" | sed "s/value: \"0\"/value: \"${{ github.event.number }}\"/" | oc apply -f -
+
+ scale-up-legacy:
+ name: Scale up legacy
+ needs: [recreate-database]
+ environment: dev
+ if: always()
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Start the Legacy Service
+ continue-on-error: true
+ run: |
+ oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }}
+ oc project ${{ secrets.OC_NAMESPACE }} # Safeguard!
+ oc scale dc/nr-forest-client-${{ github.event.number }}-legacy --replicas=1
diff --git a/backend/openshift.configmap.dev.yml b/backend/openshift.configmap.dev.yml
index d31b88ca14..87173564b0 100644
--- a/backend/openshift.configmap.dev.yml
+++ b/backend/openshift.configmap.dev.yml
@@ -28,3 +28,9 @@ objects:
info:
app:
component: ${COMPONENT}
+ ca:
+ bc:
+ gov:
+ nrs:
+ idirMaxSubmissions: 65535
+ otherMaxSubmissions: 65535
diff --git a/cypress/README.md b/cypress/README.md
index 4e87d28fa0..873d598048 100644
--- a/cypress/README.md
+++ b/cypress/README.md
@@ -11,24 +11,28 @@ The below sections are intended to explain how the framework works, along with a
[Cucumber](https://cucumber.io/) is a popular tool used for behavior-driven development (BDD) in software testing. It allows you to write executable specifications in a natural language format called Gherkin. Gherkin is a plain-text language that is easy to understand and can be used by non-technical stakeholders as well. In this tutorial, we will guide you through the basics of writing a Cucumber/Gherkin file for end-to-end tests. To learn more about Cucumber, Gherking and BDD, you can enroll on a [free course](https://school.cucumber.io/courses/bdd-overview-for-business-analysts-and-product-owners).
-It is always good to read about Gherkin language format before start but in summary, Gherkin is a writing format that leverages plain english to allow users to describe the expected behavior in simple steps that is simple to understand by non-technical persons, but also makes sense to be executed in that specific order. Think of it like a set of steps in a cake recipe. Each test is written in a file with the `.feature` extension and is saved inside [cypress/e2e](cypress/e2e) folder on this repo, you can check some of the existing files for reference.
+It is always good to read about Gherkin language format before start but in summary, Gherkin is a writing format that leverages plain english to allow users to describe the expected behavior in simple steps that is simple to understand by non-technical persons, but also makes sense to be executed in that specific order. Think of it like a set of steps in a cake recipe.
- Avoid using names with spaces or special characters. Replace spaces with `_` and special characters for it's non-special character counterpart
+To make things easier for non-technical team members, we developed a strategy to leverage the use of [github issues](https://github.com/bcgov/nr-forest-client/issues), something that is quite similar to Jira Tickets. Click on `New issue` and a list of possible issue types will appear, select the `User provided automated test-case` one, and a form will appear. Follow the instructions on it on how to fill up the form with the appropriate data. This will then be automatically converted to a `feature file` that will be used for test.
-Here is how to get started:
+ Pay attention to the format of the test description. Gherkin feature files are sensitive to spacing and the keywords are really picky when it comes to casing (uppercase x lowercase).
-### Step 1: Creating a Feature File
+Another thing that the development team did to facilitate the usage of Gherkin is the ready-to-use collection of instructions that can be used to speed up the writing of test cases. Check the [existing step instructions](#existing-step-instructions) topic for a list of steps already implemented.
-Create a new file with the `.feature` extension inside the [cypress/e2e](cypress/e2e) folder. This file will contain your Gherkin scenarios. Start by writing a short description of the feature you are testing, preceded by the Feature keyword. For example:
+Without further ado, let's get started:
+
+### Creating a Feature
+
+Every test group is called a `Feature` on Gherkin. This is the first keyword here and it will have a meaningful name. This will contain your scenarios and it can include a short description of the feature you are testing, preceded by the Feature keyword. For example:
```gherkin
Feature: User Registration
- As a new user
- I want to register on the website
- So that I can access exclusive content
+ As a new user I want to register on the website so that I can access exclusive content
```
-### Step 2: Writing Scenarios
+ Be aware that the description should be a level deeper than the Feature itself. You can use tab or two spaces
+
+### Creating Scenarios
Scenarios represent specific test cases. Each scenario consists of a series of steps. Steps can be one of the following: **Given**, **When**, **Then**, **And**, or **But**. Here's an example scenario for our user registration feature:
@@ -40,14 +44,101 @@ Scenario: Successful user registration
Then I should see a success message
```
-Congratulations! You have successfully written a basic Cucumber/Gherkin file for end-to-end tests. You can continue adding more scenarios and step definitions to cover different test cases in the application.
+ Be aware that each step should be a level deeper than the Scenario itself. You can use tab or two spaces
+ Also, keep in mind that this should be at least one level deeper than the feature above it.
+
+Here's the final product of the above feature and scenario combined.
+
+```gherkin
+Feature: User Registration
+ As a new user I want to register on the website so that I can access exclusive content
+
+ Scenario: Successful user registration
+ Given I am on the registration page
+ When I fill in the registration form with valid information
+ And I click the "Register" button
+ Then I should see a success message
+```
+
+Congratulations! You have successfully written a basic Cucumber/Gherkin end-to-end tests. You can continue adding more scenarios and step definitions to cover different test cases in the application.
Remember, the power of Cucumber lies in its ability to bridge the communication gap between technical and non-technical team members, enabling collaboration and providing a common language for defining software behavior.
+### Existing step instructions
+
+The development team created a set of pre-defined step definitions that can be used to leverage the use of Gherkin tests and speed up the adoption of it with non-technical team members. The below steps should be used `as-is`, without changing the case of any letter, except for `variables`.
+
+A variable is a piece of information that will be used to pass down a information. Every variable should be wrapped in double quotes (`"`). We will have two distinc group of variables described below, and they will be defined as `input` and `field name`. **Input** variables are the actual data that you want to select or insert in the form, such as the first name `James` or the `Individual` type of user. **Field name** is a type of variable used to identify a field in the form based on it's label name. Some examples are `First name` and `Client type`. They should have the exact name and casing as the form, otherwise the test won't be able to find the input to fill the data in.
+
+ The below list of instructions can be used in any of the steps, such as `Given`, `When`, `Then`, `And` or `But`. They are case sensitive and should be used as they are.
+
+Also, to speed up the process and avoid credentials being leaked everywhere, we have a special instruction that will be used to login using some specific user types. This instruction uses the `annotation` and it should be used at the `scenario` level. For more information, please refer to the [credentials](#credentials) topic below.
+
+Here's a list of instructions that are already implemented and can be used:
+
+| Step | Variables | Description | Example |
+| --------------- | ------------------ | ------------------------------- | ------------------ |
+| I visit {input} | `input` as the URL | Navigate to a specific URL path | I visit "/landing" |
+| I can read {input} | `input` as the text on the screen | Look up for a specific text on the screen | I can read "Create new client" |
+| I cannot see {input} | `input` as the content of something that should not be on the screen | Look up for a specific text or component that should not be on the screen | I cannot see "Error" |
+| I wait for the text {input} to appear | `input` as the text to be waited to appear on the screen | Wait for a specific text to appear on the screen | I wait for the text "Success" to appear |
+| I click on the {field name} button | `field name` as the text/name of the button to be clicked | Finds and click a button | I click on the "Next" button |
+| I click on next | - | Click on the next button. It is a variation of the button click, with a limited scope | I click on next |
+| I submit | - | Submit the form at the end. Is a variation of the button click, but with a more limited scope | I submit |
+| I type {input} into the {field name} form input | `input` as the data to be inserted and `field name` as the field name, based on a label | Insert data into a input text field | I type "James" into the "First name" form input |
+| I clear the {field name} form input | `field name` as the field name, based on a label | Clear the content of a input text field | I clear the "First name" form input |
+| I type {input} into the {field name} form input for the {field name} | `input` as the data to be inserted and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Insert data into a input text field that belongs to a specific group | I type "James" into the "First name" form input for the "Primary contact" |
+| I replace the {field name} with {input} form input | `field name` as the field name, based on a label and `input` as the data to be inserted | Replace the content of a input text field | I replace the "First name" with "John" form input |
+| I replace the {field name} with {input} form input for the {field name} | `field name` as the field name, based on a label and `input` as the data to be inserted and the last `field name` is a reference for the group where this information should be inserted. | Replace the content of a input text field | I replace the "First name" with "John" form input |
+| I type {input} into the {field name} form input area | `input` as the data to be inserted and `field name` as the field name, based on a label | Insert data into a input area | I type "All good" into the "Notes" form input area |
+| I clear the {field name} form input area | `field name` as the field name, based on a label | Clear the content of a input area | I clear the "First name" form input area |
+| I type {input} into the {field name} form input area for the {field name} | `input` as the data to be inserted and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Insert data into a input area | I type "All good" into the "Notes" form input area for the "Primary contact" |
+| I select {input} from the {field name} form input | `input` as the data to be selected and `field name` as the field name, based on a label | Select a option from a dropdown | I select "Individual" from the "Client type" form input |
+| I select {input} from the {field name} form input for the {field name} | `input` as the data to be selected and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Select a option from a dropdown | I select "Billing" from the "Client type" form input for the "Primary contact" |
+| I select {input} from the {field name} multiselect | `input` as the data to be selected and `field name` as the field name, based on a label | Select a option from a multi select dropdown | I select "Individual" from the "Contact type" multiselect |
+| I select {input} from the {field name} multiselect for the {field name} | `input` as the data to be selected and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Select a option from a multi select dropdown | I select "Office" from the "Location name" multiselect for the "Primary contact|
+| I type {input} and select {input} from the {field name} form autocomplete | `input` as the data to be inserted, being the first one the text to be typed and the second one the text to be selected and `field name` as the field name, based on a label | Type into the autocomplete and selects one of the possible results | I type "James" and select "James Bond" from the "Client name" form autocomplete |
+| I type {input} and select {input} from the {field name} form autocomplete for the {field name} | `input` as the data to be inserted, being the first one the text to be typed and the second one the text to be selected and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Type into the autocomplete and selects one of the possible results | I type "2975 Jutl" and select "2975 Jutland Rd" from the "Street address or PO box" form autocomplete for the "Primary location |
+| I add a new location called {input} | `input` as the name of the location to be added | Add a new location to the list | I add a new location called "New York Office" |
+| I addd a new contact called {input} | `input` as the name of the contact to be added | Add a new contact to the list | I add a new contact called "Johnathan Wick" |
+| I should see the {input} message {input} on the {field name} | `input` as the type of message, being **error** or **warning**, `input` as the text of the message (can be partial) and `field name` as the location/group where the notification should appear, such as the **top**, **Primary contact** or **Office** | Check if a specific message is displayed on the screen | I should see the "error" message "Invalid email" on the "top" |
+| The field {field name} should have the {input} message {input} | `field name` as the field name, based on a label, `input` as the type of message, being **error** or **warning**, `input` as the text of the message (can be partial) | Check if a specific message is displayed on a specific field | The field "Email" should have the "error" message "Invalid email" |
+| I fill the form as follows | - | This is a special instruction that will be followed by a table with the data to be inserted in the form. More on the [data tables](#data-tables) topic below | I fill the form as follows |
+| I fill the {input} address with the following | `input` as the location name value, or `Primary location` for the primary location | This is a special instruction that will be followed by a table with the data to be inserted in the form. More on the [data tables](#data-tables) topic below | I fill the "Primary location" address with the following |
+| I fill the {input} information with the following | `input` as the location or contact name value, or `Primary contact` for the primary contact | This is a special instruction that will be followed by a table with the data to be inserted in the form. More on the [data tables](#data-tables) topic below | I fill the "Johnathan Wick" information with the following |
+| I mark {input} on the {field name} {input} input | `input` as the value to be marked, `field name` as the field name, based on a label and the last `input` as the type of the input, being **radio** or **checkbox** | Mark a specific input | I mark "Yes" on the "Primary contact" "radio" input |
+| The {field name} component is using the font {input} for the {field name} | `field name` as the field name, based on a label and `input` as the font name and the last `field name` is a reference for the group where this information should be inserted. | Check if a specific font is being used on a component | The "Business information" component is using the font "Arial" |
+| The {input} has weight {input} inside the {field name} | `input` as the text to be checked, `input` as the font weight and `field name` as the location/group where the text should be | Check if a specific font weight is being used on a text | The "Business information" has weight "bold" inside the "form" |
+| The {input} size is {input} inside the {field name} | `input` as the text to be checked, `input` as the font size and `field name` as the location/group where the text should be | Check if a specific font size is being used on a text | The "Business information" size is "16px" inside the "form" |
## For Developers
-The developer will implement one `.ts` file for each `.feature` file, using the same name. Ex: `sample.feature` will have a corresponding `sample.ts` file.
+Each test is written in a file with the `.feature` extension and is saved inside [cypress/e2e](cypress/e2e) folder on this repo, you can check some of the existing files for reference. The developer will implement one `.ts` file for each `.feature` file, using the same name. Ex: `sample.feature` will have a corresponding `sample.ts` file. This `.ts` file is only required if the `.feature` file has any instruction that is too specific to be implemented as a common step verbate.
+
+ Avoid using names with spaces or special characters. Replace spaces with `_` and special characters for it's non-special character counterpart
+
+### Creating a Feature File
+
+Create a new file with the `.feature` extension inside the [cypress/e2e](cypress/e2e) folder. This file will contain your Gherkin scenarios. Start by writing a short description of the feature you are testing, preceded by the Feature keyword. For example:
+
+```gherkin
+Feature: User Registration
+ As a new user
+ I want to register on the website
+ So that I can access exclusive content
+```
+
+### Writing Scenarios
+
+Scenarios represent specific test cases. Each scenario consists of a series of steps. Steps can be one of the following: **Given**, **When**, **Then**, **And**, or **But**. Here's an example scenario for our user registration feature:
+
+```gherkin
+Scenario: Successful user registration
+ Given I am on the registration page
+ When I fill in the registration form with valid information
+ And I click the "Register" button
+ Then I should see a success message
+```
### Writing Step Definitions
@@ -81,4 +172,57 @@ You can use the deployed application for the test, or the local environment. Ide
After the tests are executed, it will generate a few artefacts as proof of execution, such as screenshots and videos. This is good for reference, in case of an error, or to validate a scenario with the rest of the team.
-When a feature file exists without the corresponding implementation, it will make the automated test fail. **DON'T PANIC**. This is the expected behavior if you're adding a new scenario. One of the developers will be notified when new things are created so they can deal with the implementation of that scenario.
\ No newline at end of file
+When a feature file has a instruction without the corresponding implementation step, it will make the automated test fail. **DON'T PANIC**. This is a expected behavior if you're adding a new instruction. One of the developers will be notified when new things are created so they can deal with the implementation of that specific step.
+
+## Data tables
+
+Data tables makes the form filling process way easier and faster. It allows you to insert multiple data at once, without the need to write each step individually. The data table should be inserted after the `I fill the form as follows` instruction, and it should have 3 columns, the first one being the field name, the second one the data to be inserted and the third one the kind of field.
+
+The kind of field is a special instruction that will be used to identify the type of field that is being filled. The possible values are `text`, `select`, `multiselect`, `autocomplete` and `textbox`.
+
+Here's an example of a data table:
+
+```gherkin
+Scenario: Submit individuals
+ When I click on the "Create client" button
+ And I can read "Create client"
+ Then I select "Individual" from the "Client type" form input
+ And I fill the form as follows
+ | Field name | Value | Type |
+ | First name | James | text |
+ | Last name | Baxter | text |
+ | Year | 1990 | text |
+ | Month | 01 | text |
+ | Day | 01 | text |
+ | ID type | Canadian driver's licence | select |
+ | ID number | 4417845 | text |
+```
+
+Pay attention to the way the data table is structured, as it is very sensitive to spacing and the number of columns. The `Type` column should be written in lowercase, the `Field name` should be the exact name of the field in the form and the `Value` should be the data to be inserted or selected. The table itself is composed by the `|` character to separate the columns and the rows, it is required to have a `|` at the beginning and at the end of each row. Also pay attention to the spacing required to make the table work, as this follows the Gherkin language format.
+
+One important case here to note is that for the `address` and `contact` information groups, the `contact` or `address` should be created/enabled first. If you declare a `contact` information before the `contact` group is created, the test will fail. The same applies for the `address` group.
+
+For `address` and `contact` we have a special instruction that can reduce the amount of steps required to start the new group.
+
+## Credentials
+
+To avoid credentials being leaked everywhere, we have a special instruction that will be used to login using some specific user types. This instruction uses the `annotation` and it should be used at the `scenario` level. The annotation should be written in the following format:
+
+```gherkin
+@loginAsEditor
+Scenario: Submit individuals
+ When I click on the "Create client" button
+ And I can read "Create client"
+```
+
+The `@loginAsEditor` is a special instruction that will be used to login as a specific user type. The `@` symbol is required to be used before the instruction, and it should be placed at the beginning of the scenario. The instruction itself should be written in camelCase, with the first letter of each word in uppercase. The instruction should be written right above the `Scenario` keyword. The possible values for it can be found down below. Keep in mind that it will only login and it will stop right after the login is done, on the expected page.
+
+Here's a list of possible instructions that can be used:
+
+| Instruction | Description |
+| ---------------- | --------------------------------------|
+| @loginAsEditor | Login as a staff editor user type |
+| @loginAsViewer | Login as a staff viewer user type |
+| @loginAsAdmin | Login as a staff admin user type |
+| @loginAsBCeID | Login as a BCeID user type |
+| @loginAsBCSC | Login as a BC Services Card user type |
diff --git a/cypress/cypress.config.ts b/cypress/cypress.config.ts
index f7d093e203..48cd453a58 100644
--- a/cypress/cypress.config.ts
+++ b/cypress/cypress.config.ts
@@ -48,7 +48,17 @@ async function setupNodeEvents(
export default defineConfig({
e2e: {
+ reporter: require.resolve("@badeball/cypress-cucumber-preprocessor/pretty-reporter"),
specPattern: "**/*.feature",
setupNodeEvents,
+ defaultCommandTimeout: 10000,
+ pageLoadTimeout: 60000,
+ },
+ includeShadowDom: true,
+ viewportHeight: 1080,
+ viewportWidth: 1920,
+ retries: {
+ runMode: 3,
+ openMode: 0,
},
});
\ No newline at end of file
diff --git a/cypress/cypress/e2e/bceid.feature b/cypress/cypress/e2e/bceid.feature
new file mode 100644
index 0000000000..ffcb28915d
--- /dev/null
+++ b/cypress/cypress/e2e/bceid.feature
@@ -0,0 +1,67 @@
+Feature: BceID User Tests
+
+ Deals with BCeID Business user scenarios
+
+ @loginAsBCeID
+ Scenario: BceID User Login
+ Given I am a "BceID" user
+ When I can read "New client application"
+
+ @loginAsBCeID
+ Scenario: BceID Unregistered User
+ Given I am a "BceID" user
+ When I can read "New client application"
+ And I select "DMH - 100 Mile House Natural Resource District" from the "District" form input
+ Then I mark "I have an unregistered sole proprietorship" on the "Type of business" "radio" input
+ And I fill the form as follows
+ | Field name | Value | Type |
+ | Year | 1990 | text |
+ | Month | 01 | text |
+ | Day | 01 | text |
+ Then I click on next
+ And I type "2975 Jutland Rd" and select "2975 Jutland Rd" from the "Street address or PO box" form autocomplete
+ Then I click on next
+ And I type "2255522552" into the "Phone number" form input
+ Then I select "Billing" from the "Primary role" form input
+ And I add a new contact called "John Wick"
+ And I fill the "John Wick" information with the following
+ | Field name | Value | Type |
+ | Email address | jwick@thecontinental.ca | text |
+ | Phone number | 2501234568 | text |
+ | Primary role | Director | select |
+ And I click on next
+ Then I submit
+ And I can read "Application submitted!"
+
+ @loginAsBCeID
+ Scenario: BceID Registered User
+ Given I am a "BceID" user
+ When I can read "New client application"
+ And I select "DMH - 100 Mile House Natural Resource District" from the "District" form input
+ Then I mark "I have a BC registered business (corporation, sole proprietorship, society, etc.)" on the "Type of business" "radio" input
+ And I type "veitch f" and select "VEITCH FOREST" from the "BC registered business name" form autocomplete
+ And I fill the form as follows
+ | Field name | Value | Type |
+ | Year | 1990 | text |
+ | Month | 01 | text |
+ | Day | 01 | text |
+ Then I click on next
+ And I click on next
+ And I type "2255522552" into the "Phone number" form input
+ Then I select "Billing" from the "Primary role" form input
+ And I fill the "GARY VEITCH" information with the following
+ | Field name | Value | Type |
+ | Email address | garyveitch@mail.ca | text |
+ | Phone number | 7787787778 | text |
+ | Primary role | Director | select |
+ And I click on next
+ Then I submit
+ And I can read "Application submitted!"
+
+ @loginAsBCeID
+ Scenario: BceID Unregistered User already registered
+ Given I am a "BceID" user
+ When I can read "New client application"
+ And I select "DMH - 100 Mile House Natural Resource District" from the "District" form input
+ Then I mark "I have an unregistered sole proprietorship" on the "Type of business" "radio" input
+ And I should see the "error" message "Looks like “01-DEV, LOAD” has a client number. Select the 'Receive email and logout' button below to have it sent to you at maria.martinez@gov.bc.ca" on the "Business information"
\ No newline at end of file
diff --git a/cypress/cypress/e2e/bceid.ts b/cypress/cypress/e2e/bceid.ts
new file mode 100644
index 0000000000..52d07c0fae
--- /dev/null
+++ b/cypress/cypress/e2e/bceid.ts
@@ -0,0 +1,2 @@
+import { Then, Given, Step } from "@badeball/cypress-cucumber-preprocessor";
+//This file is left intentionally empty
diff --git a/cypress/cypress/e2e/landing.feature b/cypress/cypress/e2e/landing.feature
new file mode 100644
index 0000000000..cfd00316c6
--- /dev/null
+++ b/cypress/cypress/e2e/landing.feature
@@ -0,0 +1,27 @@
+Feature: Check application login
+
+ This validate the possibility of login with any kind of provider
+
+ Scenario: Try to log with BCeID
+ Given I visit "/landing"
+ Then I can read "Client Management System"
+ Then I cannot see "Log in with BCeID"
+ Then I can read "Log in with IDIR"
+ Then I visit "/landing?fd_to=&ref=external"
+ Then I can read "Log in with"
+
+ Scenario: Try to log with BCSC
+ Given I visit "/landing"
+ Then I can read "Client Management System"
+ Then I cannot see "Log in with BC Services Card"
+ Then I can read "Log in with IDIR"
+ Then I visit "/landing?fd_to=&ref=individual"
+ Then I can read "Continue with"
+ Then I can read "BC Services Card app"
+ Then I can read "Test with username and password"
+
+ Scenario: Try to log with IDIR
+ Given I visit "/landing"
+ Then I can read "Client Management System"
+ And I click on the "Log in with IDIR" button
+ Then I can read "Log in with IDIR"
diff --git a/cypress/cypress/e2e/landing.ts b/cypress/cypress/e2e/landing.ts
new file mode 100644
index 0000000000..3d99c031ec
--- /dev/null
+++ b/cypress/cypress/e2e/landing.ts
@@ -0,0 +1,2 @@
+import { Then, Given } from "@badeball/cypress-cucumber-preprocessor";
+//This file is left intentionally empty
\ No newline at end of file
diff --git a/cypress/cypress/e2e/logouts.feature b/cypress/cypress/e2e/logouts.feature
new file mode 100644
index 0000000000..79fdd40258
--- /dev/null
+++ b/cypress/cypress/e2e/logouts.feature
@@ -0,0 +1,9 @@
+Feature: BceID User Tests
+
+ @loginAsBCeID
+ Scenario: BceID User Logout
+ Given I am a "BceID" user
+ When I can read "New client application"
+ Then I click on the "Logout" button
+ And I can read "Are you sure you want to logout? Your data will not be saved."
+ Then I click on the "Logout" button
diff --git a/cypress/cypress/e2e/sample.feature b/cypress/cypress/e2e/sample.feature
index 92760d2139..1f441cf0b8 100644
--- a/cypress/cypress/e2e/sample.feature
+++ b/cypress/cypress/e2e/sample.feature
@@ -3,5 +3,5 @@ Feature: Form screen loads correctly
This is just a simple template file to show how to write and format your test
Scenario: Screen loads
- Given I am on the form page
- Then I can see the title
+ Given I visit "/"
+ Then I can read "Client Management System"
diff --git a/cypress/cypress/e2e/sample.ts b/cypress/cypress/e2e/sample.ts
index b86902accb..bf60a1fc16 100644
--- a/cypress/cypress/e2e/sample.ts
+++ b/cypress/cypress/e2e/sample.ts
@@ -1,9 +1,2 @@
-import { Then, Given } from "@badeball/cypress-cucumber-preprocessor";
-
-Given("I am on the form page", () => {
- cy.visit('/');
-});
-
-Then("I can see the title", () => {
- cy.contains('Client Management System')
-});
+import { Then, Given, Step } from "@badeball/cypress-cucumber-preprocessor";
+//This file is left intentionally empty
\ No newline at end of file
diff --git a/cypress/cypress/e2e/staffCreate.feature b/cypress/cypress/e2e/staffCreate.feature
new file mode 100644
index 0000000000..2df58e6bc5
--- /dev/null
+++ b/cypress/cypress/e2e/staffCreate.feature
@@ -0,0 +1,95 @@
+Feature: Staff Screens
+
+ This feature file is to test staff screen use cases
+
+ @loginAsEditor
+ Scenario: Submit individuals
+ When I click on the "Create client" button
+ And I can read "Create client"
+ Then I select "Individual" from the "Client type" form input
+ And I fill the form as follows
+ | Field name | Value | Type |
+ | First name | James | text |
+ | Last name | Baxter | text |
+ | Year | 1990 | text |
+ | Month | 01 | text |
+ | Day | 01 | text |
+ | ID type | Canadian driver's licence | select |
+ | ID number | 4417845 | text |
+ Then I click on next
+ And I fill the "Primary location" address with the following
+ | Field name | Value | Type |
+ | Location name | Office | text |
+ | Email address | jamesbaxter@mail.ca | text |
+ | Primary phone number | 2501231568 | text |
+ | Secondary phone number | 2501233568 | text |
+ | Fax | 2501239568 | text |
+ | Street address or PO box | 1520 Blanshard St | autocomplete |
+ Then I click on next
+ And I fill the "Primary contact" information with the following
+ | Field name | Value | Type |
+ | Email address | baxter.james@mail.ca | text |
+ | Primary phone number | 2501237567 | text |
+ | Secondary phone number | 2501232567 | text |
+ | Fax | 2501444567 | text |
+ | Contact type | Billing | select |
+ | Location name | Office | multiselect |
+ Then I click on next
+ Then I submit
+ And I wait for the text "has been created!" to appear
+
+ @loginAsEditor
+ Scenario: Editor can submit registered
+ When I click on the "Create client" button
+ And I can read "Create client"
+ Then I select "BC registered business" from the "Client type" form input
+ And I type "star dot star" and select "STAR DOT STAR VENTURES" from the "Client name" form autocomplete
+ Then I wait for the text "This information is from BC Registries" to appear
+ Then I click on next
+ And I fill the "Primary location" address with the following
+ | Field name | Value | Type |
+ | Street address or PO box | 1515 Blanshard | autocomplete |
+ | Email address | mail2@mail.ca | text |
+ | Primary phone number | 7780000001 | text |
+ | Secondary phone number | 7780000002 | text |
+ | Notes | This is a test | textbox |
+ And I add a new location called "Home"
+ And I fill the "Home" address with the following
+ | Field name | Value | Type |
+ | Street address or PO box | 1515 Blanshard | autocomplete |
+ Then I click on next
+ And I fill the "Primary contact" information with the following
+ | Field name | Value | Type |
+ | Email address | mail3@mail.ca | text |
+ | Primary phone number | 7780000003 | text |
+ | Contact type | Billing | select |
+ | Location name | Home | multiselect |
+ | Location name | Mailing address | multiselect |
+ And I fill the "MARCEL ST. AMANT" information with the following
+ | Field name | Value | Type |
+ | Email address | mail3@mail.ca | text |
+ | Primary phone number | 7780000004 | text |
+ | Contact type | Billing | select |
+ | Location name | Home | multiselect |
+ | Last name | ST AMANT | textreplace |
+ And I click on next
+ Then I submit
+ And I wait for the text "has been created!" to appear
+
+ @loginAsEditor
+ Scenario: Already exists and has fuzzy match
+ When I click on the "Create client" button
+ And I can read "Create client"
+ Then I select "Individual" from the "Client type" form input
+ And I fill the form as follows
+ | Field name | Value | Type |
+ | First name | James | text |
+ | Last name | Baxter | text |
+ | Year | 1959 | text |
+ | Month | 05 | text |
+ | Day | 18 | text |
+ | ID type | Canadian driver's licence | select |
+ | ID number | 1234567 | text |
+ Then I click on next
+ And I should see the "warning" message "was found with similar name and birthdate" on the "top"
+ And The field "First name" should have the "warning" message "There's already a client with this name"
\ No newline at end of file
diff --git a/cypress/cypress/e2e/staffCreate.ts b/cypress/cypress/e2e/staffCreate.ts
new file mode 100644
index 0000000000..0bd01d6e8f
--- /dev/null
+++ b/cypress/cypress/e2e/staffCreate.ts
@@ -0,0 +1,2 @@
+import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
+//This file is left intentionally empty
diff --git a/cypress/cypress/support/commands.ts b/cypress/cypress/support/commands.ts
index 072e8c751d..92d6aa769f 100644
--- a/cypress/cypress/support/commands.ts
+++ b/cypress/cypress/support/commands.ts
@@ -1,71 +1,42 @@
/* eslint-disable no-undef */
///
-
-const generateRandomHex = (length: number): string => {
- const characters = '0123456789abcdef'
- let result = ''
- for (let i = 0; i < length; i++) {
- const randomIndex = Math.floor(Math.random() * characters.length)
- result += characters.charAt(randomIndex)
- }
- return result
-}
-
-Cypress.Commands.add('addCookie', (name: string, value: string) => {
- cy.setCookie(name, value, {
- domain: 'localhost',
- path: '/',
- httpOnly: true,
- secure: true,
- expiry: Date.now() + 86400000
- })
-})
-
-Cypress.Commands.add('expireCookie', (name: string) => {
- cy.setCookie(name, '', {
- domain: 'localhost',
- path: '/',
- httpOnly: true,
- secure: true,
- expiry: Date.now() - 86400000 * 2
- })
-})
-
-Cypress.Commands.add('addToSessionStorage', (key: string, value: any) => {
- cy.window().then((win) => {
- win.sessionStorage.setItem(key, JSON.stringify(value))
- })
-})
-
-Cypress.Commands.add('expireSessionStorage', (key: string) => {
- cy.window().then((win) => {
- win.sessionStorage.removeItem(key)
- })
-})
-
-Cypress.Commands.add('addToLocalStorage', (key: string, value: any) => {
- cy.window().then((win) => {
- win.localStorage.setItem(key, JSON.stringify(value))
- })
-})
-
-Cypress.Commands.add('expireLocalStorage', (key: string) => {
- cy.window().then((win) => {
- win.localStorage.removeItem(key)
- })
-})
-
-Cypress.Commands.add('login', (email: string, name: string) => {
- cy.get('.landing-button').should('be.visible')
- cy.addToSessionStorage('user', {
- name,
- provider: 'idir',
- userId: generateRandomHex(32),
- email,
- firstName: 'UAT',
- lastName: 'Test'
- })
- cy.reload()
- cy.wait(1000)
-})
\ No newline at end of file
+Cypress.Commands.add("logout", () => {
+ cy.get("[data-id=logout-btn]").should("be.visible");
+});
+
+Cypress.Commands.add("checkAutoCompleteErrorMessage", (field: string, message: string) => {
+ cy.get(field)
+ .should('have.attr', 'aria-invalid', 'true')
+ .should('have.attr', 'invalid-text', message);
+
+ cy.get(field)
+ .shadow()
+ .find('svg').should('exist');
+
+ cy.get(field)
+ .shadow()
+ .find('div.cds--form__helper-text > slot#helper-text')
+ .invoke('text')
+ .should('contains', message);
+});
+
+Cypress.Commands.add("checkAccordionItemState", (additionalSelector: string, open: boolean) => {
+ cy.get(`cds-accordion-item${additionalSelector}`).should(
+ `${open ? "" : "not."}have.attr`,
+ "open",
+ );
+});
+
+Cypress.Commands.add('waitForPageLoad', (element: string) => {
+ cy.get(element).should('be.visible').then(() => {
+ cy.log('Page loaded');
+ });
+});
+
+Cypress.Commands.add('logAndScreenshot', (message: string) => {
+ cy.log(message).then(() => {
+ console.log(message);
+ cy.screenshot(`log-${Date.now()}`); // Takes a screenshot with a timestamp
+ });
+});
diff --git a/cypress/cypress/support/cypress.d.ts b/cypress/cypress/support/cypress.d.ts
index e1f89e1433..e6e3bb9e32 100644
--- a/cypress/cypress/support/cypress.d.ts
+++ b/cypress/cypress/support/cypress.d.ts
@@ -1,11 +1,9 @@
declare namespace Cypress {
interface Chainable {
- login(email: string, name: string): Chainable;
- addCookie(name: string, value: string): Chainable;
- addToLocalStorage(key: string, value: any): Chainable;
- expireLocalStorage(key: string): Chainable;
- addToSessionStorage(key: string, value: any): Chainable;
- expireSessionStorage(key: string): Chainable;
- expireCookie(name: string): Chainable;
+ logout(): Chainable;
+ checkAutoCompleteErrorMessage(field: string, message: string): Chainable;
+ checkAccordionItemState(additionalSelector: string, open: boolean): Chainable;
+ waitForPageLoad(element: string): Chainable;
+ logAndScreenshot(message: string): Chainable;
}
}
\ No newline at end of file
diff --git a/cypress/cypress/support/e2e.ts b/cypress/cypress/support/e2e.ts
index 43c03b759d..de527d4507 100644
--- a/cypress/cypress/support/e2e.ts
+++ b/cypress/cypress/support/e2e.ts
@@ -1 +1,14 @@
import './commands'
+
+Cypress.on('window:before:load', (win) => {
+ // Listen to browser console logs and pass them to the Cypress console
+ const originalConsoleLog = win.console.log;
+ win.console.log = (...args) => {
+ originalConsoleLog(...args);
+ // Pass logs to Cypress terminal
+ Cypress.log({
+ name: 'console.log',
+ message: [...args],
+ });
+ };
+});
\ No newline at end of file
diff --git a/cypress/cypress/support/step_definitions/area_input_steps.ts b/cypress/cypress/support/step_definitions/area_input_steps.ts
new file mode 100644
index 0000000000..368f597a83
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/area_input_steps.ts
@@ -0,0 +1,44 @@
+import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+
+let idir = true;
+
+BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () {
+ idir = false;
+});
+BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () {
+ idir = true;
+});
+
+/* Area Input Steps */
+
+Then('I type {string} into the {string} form input area', (text: string, input: string) => {
+ cy.contains('div.cds-text-input-label span', input).then(($label) => {
+ cy.wrap($label.parent().parent().parent())
+ .find('textarea[id*="input"]')
+ .type(text);
+ });
+});
+
+Then('I clear the {string} form input area', (input: string) => {
+ cy.contains('div.cds-text-input-label span', input).then(($label) => {
+ cy.wrap($label.parent().parent().parent())
+ .find('textarea[id*="input"]')
+ .clear();
+ });
+});
+
+Then(
+ 'I type {string} into the {string} form input area for the {string}',
+ (value: string, fieldLabel: string, sectionTitle: string) => {
+
+ if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') {
+ cy.get('div.frame-01:first').within(() => {
+ Step(this, `I type "${value}" into the "${fieldLabel}" form input area`);
+ });
+ } else {
+ cy.get(`[data-text="${sectionTitle}"]`).within(() => {
+ Step(this, `I type "${value}" into the "${fieldLabel}" form input area`);
+ });
+ }
+
+});
diff --git a/cypress/cypress/support/step_definitions/autocomplete_steps.ts b/cypress/cypress/support/step_definitions/autocomplete_steps.ts
new file mode 100644
index 0000000000..948480ba1b
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/autocomplete_steps.ts
@@ -0,0 +1,62 @@
+import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+
+let idir = true;
+
+BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () {
+ idir = false;
+});
+BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () {
+ idir = true;
+});
+
+/* Autocomplete Input Steps */
+
+Then('I type {string} and select {string} from the {string} form autocomplete', (search: string, value: string, input: string) => {
+
+ if(input === 'Client name' || input === 'BC registered business name') {
+ cy.intercept('GET', `**/api/clients/**`).as('autocomplete');
+ cy.intercept('GET', `**/api/opendata/**`).as('autocomplete');
+ } else if(input === 'Street address or PO box') {
+ cy.intercept('GET', `**/api/address**`).as('autocomplete');
+ }
+
+ cy.contains('label', input).then(($label) => {
+ const parentShadow = $label[0].getRootNode();
+ cy.wrap(parentShadow)
+ .find('input')
+ .type(search, { delay: 150 })
+ .then(() => {
+ cy.wait('@autocomplete');
+ cy.wrap(parentShadow)
+ .parent()
+ .find(`cds-combo-box-item[data-value="${value}"], cds-combo-box-item[data-value^="${value}"]`)
+ .first()
+ .then(($item) => {
+ if ($item.length) {
+ cy.wrap($item).click();
+ } else {
+ throw new Error(`Item with value "${value}" not found.`);
+ }
+ })
+ .then(() => {
+ cy.wait('@autocomplete');
+ });
+ });
+ });
+});
+
+Then(
+ 'I type {string} and select {string} from the {string} form autocomplete for the {string}',
+ (search: string, value: string, fieldLabel: string, sectionTitle: string) => {
+
+ if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') {
+ cy.get('div.frame-01:first').within(() => {
+ Step(this, `I type "${search}" and select "${value}" from the "${fieldLabel}" form autocomplete`);
+ });
+ } else {
+ cy.get(`[data-text="${sectionTitle}"]`).within(() => {
+ Step(this, `I type "${search}" and select "${value}" from the "${fieldLabel}" form autocomplete`);
+ });
+ }
+
+});
\ No newline at end of file
diff --git a/cypress/cypress/support/step_definitions/button_steps.ts b/cypress/cypress/support/step_definitions/button_steps.ts
new file mode 100644
index 0000000000..f8c9b4731f
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/button_steps.ts
@@ -0,0 +1,74 @@
+import { When, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+
+let idir = true;
+
+BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () {
+ idir = false;
+});
+BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () {
+ idir = true;
+});
+
+/* Button Step */
+
+When('I click on the {string} button', (name: string) => {
+ buttonClick(name, ['input', 'button', 'cds-button', 'cds-modal-footer-button', 'cds-side-nav-link']);
+});
+
+When('I click on next', () => {
+
+ if (!idir) {
+ cy.get('cds-button[data-text="Next"]').click().then(() => {cy.wait(15);});
+ } else {
+ cy.intercept('POST', `**/api/clients/matches`).as('matches');
+ cy.get('cds-button[data-text="Next"]').click().then(() => {cy.wait('@matches',{ timeout: 10 * 1000 });});
+ }
+
+});
+
+When('I submit', () => {
+ if(idir){
+ cy.intercept('POST', `**/api/clients/submissions/staff`).as('submit');
+ } else {
+ cy.intercept('POST', `**/api/clients/submissions`).as('submit');
+ }
+ cy.get('cds-button[data-text="Submit"]').scrollIntoView().click().then(() => {cy.wait('@submit',{ timeout: 60 * 1000 });});
+});
+
+
+const buttonClick = (
+ name: string,
+ kinds: string[],
+ waitForIntercept: string = null,
+ waitForTime : number = 15,
+ selector: string = 'body'
+) => {
+ if (kinds.length === 0) {
+ throw new Error(`Button with label "${name}" not found.`);
+ }
+
+ // Build a selector string that matches any of the button kinds
+ const kindSelector = kinds.join(',');
+
+ cy.get(selector)
+ .find(kindSelector)
+ .filter(':visible') // Only consider visible buttons
+ .filter((index, element) => {
+ // Check for the button label in various places
+ return Cypress.$(element).attr('data-text')?.includes(name) ||
+ Cypress.$(element).html().includes(name) ||
+ Cypress.$(element).text().includes(name) ||
+ Cypress.$(element).val()?.toString().includes(name);
+ })
+ .first() // Get the first matching, visible button
+ .should('be.visible') // Ensure it's visible before clicking
+ .click() // Click the button
+ .then(() => {
+ // Handle waiting for intercept or time after clicking
+ if (waitForIntercept) {
+ cy.wait(`@${waitForIntercept}`, { timeout: waitForTime * 1000 });
+ } else if (waitForTime) {
+ cy.wait(waitForTime);
+ }
+ });
+}
diff --git a/cypress/cypress/support/step_definitions/commons.ts b/cypress/cypress/support/step_definitions/commons.ts
new file mode 100644
index 0000000000..831de3d2d9
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/commons.ts
@@ -0,0 +1,74 @@
+import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+
+let idir = true;
+
+BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () {
+ idir = false;
+});
+BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () {
+ idir = true;
+});
+
+/* Extra Actions */
+
+Then('I add a new location called {string}', (location: string) => {
+ Step(this,'I click on the "Add another location" button');
+ cy.get(`[data-text="Additional location"]`).should('be.visible');
+ Step(this,`I type "${location}" into the "Location name" form input for the "Additional location"`);
+ cy.get(`[data-text="${location}"]`).should('be.visible');
+});
+
+Then('I add a new contact called {string}', (contactName: string) => {
+ Step(this,'I click on the "Add another contact" button');
+
+ cy.get('[data-text="Additional contact"]').should('be.visible');
+ const [firstName, ...lastName] = contactName.split(' ');
+ Step(this,`I type "${firstName}" into the "First name" form input for the "Additional contact"`);
+ cy.get(`[data-text="${firstName} "]`).should('be.visible');
+ Step(this,`I type "${lastName.join(' ')}" into the "Last name" form input for the "${firstName} "`);
+
+});
+
+/* Error messages */
+
+Then('I should see the {string} message {string} on the {string}', (kind: string, message: string, location: string) => {
+ checkForActionableNotification(message, location, kind);
+});
+
+Then(
+ 'The field {string} should have the {string} message {string}',
+ (field: string, kind: string, message: string) => {
+
+ cy.contains('label', field).then(($label) => {
+ console.log('Shadow root: ', $label[0].getRootNode());
+ if(kind === 'error') {
+ cy.wrap($label[0].getRootNode())
+ .find('#invalid-text')
+ .invoke('text')
+ .should('contains', message);
+ } else {
+ cy.wrap($label[0].getRootNode())
+ .find('.cds--form-requirement')
+ .invoke('text')
+ .should('contains', message);
+ }
+ });
+
+});
+
+/* This block is dedicated to the actual code */
+
+const checkForActionableNotification = (message: string, location: string, kind: string) => {
+ let errorLookupTag = '';
+ if(location.toLowerCase().includes("top")){
+ errorLookupTag = `cds-actionable-notification[id="fuzzy-match-notification-global"][kind="${kind}"] div span.body-compact-01`;
+ } else if(idir) {
+ errorLookupTag = `cds-inline-notification[data-text="${location}"][kind="${kind}"] div span.body-compact-01`;
+ } else {
+ errorLookupTag = `cds-inline-notification[data-text="${location}"][kind="${kind}"]`;
+ }
+
+ cy.get(errorLookupTag)
+ .should('exist')
+ .should('contain.text', message);
+}
diff --git a/cypress/cypress/support/step_definitions/general_steps.ts b/cypress/cypress/support/step_definitions/general_steps.ts
new file mode 100644
index 0000000000..d6d374db8e
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/general_steps.ts
@@ -0,0 +1,35 @@
+import { Given, Then, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+BeforeStep(() => { cy.wait(10); });
+
+Given('I visit {string}', (url: string) => {
+ cy.visit(url).then(() => {
+ cy.window().then((win) => {
+ return new Cypress.Promise((resolve) => {
+ if (win.document.readyState === 'complete') {
+ resolve();
+ } else {
+ win.addEventListener('load', resolve);
+ }
+ });
+ });
+ });
+});
+
+Then('I can read {string}', (title: string) => {
+ cy.contains(title).should('be.visible');
+});
+
+Then('I cannot see {string}', (button: string) => {
+ cy.contains(button).should('not.exist');
+});
+
+Then('I wait for the text {string} to appear', (text: string) => {
+ cy.contains(text).should('be.visible');
+});
+
+Then('I wait for the text {string} to appear after {string}', (text: string,waitFor: string) => {
+ cy.wait(`@${waitFor}`,{ timeout: 10 * 1000 });
+ cy.contains(text).should('be.visible');
+});
+
+Then('I am a {string} user', (userType: string) => {});
diff --git a/cypress/cypress/support/step_definitions/loginsHooks.ts b/cypress/cypress/support/step_definitions/loginsHooks.ts
new file mode 100644
index 0000000000..d51151be8d
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/loginsHooks.ts
@@ -0,0 +1,71 @@
+import { Before, Step } from '@badeball/cypress-cucumber-preprocessor';
+
+const doLogin = (kind: string, afterLoginLocation: string, extraLandingParam: string = null) => {
+
+ const username = Cypress.env(`${kind}_username`);
+ const password = Cypress.env(`${kind}_password`);
+
+ if(!username || !password) {
+ throw new Error(`Username or password for ${kind} not found.`);
+ }
+
+ cy.session(
+ `${kind}-${username}`,
+ () => {
+ const landingPage = extraLandingParam ? `/landing?${extraLandingParam}` : '/landing';
+ // Visit the landing page
+ Step(this, `I visit "${landingPage}"`);
+
+ // Click on the login button
+ if(kind !== 'bceid' && kind !== 'bcsc') {
+ cy.waitForPageLoad('img');
+ Step(this, 'I click on the "Log in with IDIR" button');
+ } else if(kind === 'bceid') {
+ cy.waitForPageLoad('span#bceidLogo');
+ }
+
+ // Log into the application, not using a step here to prevent password spillage
+ cy.get("#user").type(username, { log: false });
+ cy.get("#password").type(password, { log: false });
+ Step(this, 'I click on the "Continue" button');
+
+ // Validate the login for session purposes
+ cy.url().should('include', afterLoginLocation);
+ cy.getCookies().then((cookies) => {
+ cookies.forEach((cookie) => cy.setCookie(cookie.name, cookie.value));
+ });
+ },
+ {
+ validate: () => {
+ cy.request(afterLoginLocation).its('status').should('eq', 200);
+ cy.visit(afterLoginLocation);
+ },
+ });
+ cy.visit(afterLoginLocation);
+
+}
+
+Before({ tags: '@loginAsEditor' }, () => {
+ doLogin('editor','/submissions');
+ cy.waitForPageLoad('cds-header');
+});
+
+Before({ tags: '@loginAsAdmin' }, () => {
+ doLogin('admin','/submissions');
+ cy.waitForPageLoad('cds-header');
+});
+
+Before({ tags: '@loginAsViewer' }, () => {
+ doLogin('viewer','/submissions');
+ cy.waitForPageLoad('cds-header');
+});
+
+Before({ tags: '@loginAsBCeID' }, () => {
+ doLogin('bceid','/new-client','ref=external');
+ cy.waitForPageLoad('cds-header');
+});
+
+Before({ tags: '@loginAsBCSC' }, () => {
+ doLogin('bcsc','/new-client-bcsc','ref=individual');
+ cy.waitForPageLoad('cds-header');
+});
\ No newline at end of file
diff --git a/cypress/cypress/support/step_definitions/radio_check_steps.ts b/cypress/cypress/support/step_definitions/radio_check_steps.ts
new file mode 100644
index 0000000000..70a65a02c7
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/radio_check_steps.ts
@@ -0,0 +1,18 @@
+import { Then, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+
+let idir = true;
+
+BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () {
+ idir = false;
+});
+BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () {
+ idir = true;
+});
+
+/* Radio and Check Input Steps */
+
+Then('I mark {string} on the {string} {string} input', (value: string, input: string, kind: string) => {
+ cy.get(`cds-radio-button-group[legend-text="${input}"]`)
+ .find(`cds-radio-button[label-text="${value}"]`)
+ .click();
+});
diff --git a/cypress/cypress/support/step_definitions/select_input_steps.ts b/cypress/cypress/support/step_definitions/select_input_steps.ts
new file mode 100644
index 0000000000..0af4f0b5f8
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/select_input_steps.ts
@@ -0,0 +1,60 @@
+import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+
+let idir = true;
+
+BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () {
+ idir = false;
+});
+BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () {
+ idir = true;
+});
+
+/* Select Input Steps */
+
+Then('I select {string} from the {string} form input', (value: string, input: string) => {
+ cy.contains('label', input).then(($label) => {
+ const parentShadow = $label[0].getRootNode();
+ cy.wrap(parentShadow).find("[part='trigger-button']").click();
+ cy.wrap(parentShadow).parent().find(`cds-combo-box-item[data-value="${value}"]`).click();
+ });
+});
+
+Then(
+ 'I select {string} from the {string} form input for the {string}',
+ (value: string, fieldLabel: string, sectionTitle: string) => {
+
+ if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') {
+ cy.get('div.frame-01:first').within(() => {
+ Step(this, `I select "${value}" from the "${fieldLabel}" form input`);
+ });
+ } else {
+ cy.get(`[data-text="${sectionTitle}"]`).within(() => {
+ Step(this, `I select "${value}" from the "${fieldLabel}" form input`);
+ });
+ }
+
+});
+
+Then('I select {string} from the {string} multiselect', (value: string, input: string) => {
+ cy.contains('label', input).then(($label) => {
+ const parentShadow = $label[0].getRootNode();
+ cy.wrap(parentShadow).find("[part='trigger-button']").click();
+ cy.wrap(parentShadow).parent().find(`cds-multi-select-item[data-value="${value}"]`).click();
+ });
+});
+
+Then(
+ 'I select {string} from the {string} multiselect for the {string}',
+ (value: string, fieldLabel: string, sectionTitle: string) => {
+
+ if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') {
+ cy.get('div.frame-01:first').within(() => {
+ Step(this, `I select "${value}" from the "${fieldLabel}" multiselect`);
+ });
+ } else {
+ cy.get(`[data-text="${sectionTitle}"]`).within(() => {
+ Step(this, `I select "${value}" from the "${fieldLabel}" multiselect`);
+ });
+ }
+
+});
diff --git a/cypress/cypress/support/step_definitions/tables_steps.ts b/cypress/cypress/support/step_definitions/tables_steps.ts
new file mode 100644
index 0000000000..8ed5fec90f
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/tables_steps.ts
@@ -0,0 +1,63 @@
+import { Then, DataTable, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+
+let idir = true;
+
+BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () {
+ idir = false;
+});
+BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () {
+ idir = true;
+});
+
+/* Data tables */
+
+Then('I fill the form as follows', (table: DataTable) => {
+ table.rows().forEach(row => {
+ const [fieldName, value, kind] = row;
+ if (kind === 'text') {
+ Step(this, `I type "${value}" into the "${fieldName.trim()}" form input`);
+ }else if (kind === 'textreplace') {
+ Step(this, `I replace the "${fieldName.trim()}" with "${value}" form input`);
+ } else if (kind === 'select') {
+ Step(this, `I select "${value}" from the "${fieldName.trim()}" form input`);
+ } else if (kind === 'autocomplete') {
+ Step(this, `I type "${value}" and select "${value}" from the "${fieldName.trim()}" form autocomplete`);
+ } else if (kind === 'multiselect') {
+ Step(this, `I select "${value}" from the "${fieldName.trim()}" multiselect`);
+ }
+ });
+});
+
+Then('I fill the {string} address with the following', (location: string, table: DataTable) => {
+ table.rows().forEach(row => {
+ const [fieldName, value, kind] = row;
+ if (kind === 'text') {
+ Step(this, `I type "${value}" into the "${fieldName.trim()}" form input for the "${location}"`);
+ } else if (kind === 'textreplace') {
+ Step(this, `I replace the "${fieldName.trim()}" with "${value}" form input for the "${location}"`);
+ } else if (kind === 'select') {
+ Step(this, `I select "${value}" from the "${fieldName.trim()}" form input for the "${location}"`);
+ } else if (kind === 'autocomplete') {
+ Step(this, `I type "${value}" and select "${value}" from the "${fieldName.trim()}" form autocomplete for the "${location}"`);
+ } else if (kind === 'multiselect') {
+ Step(this, `I select "${value}" from the "${fieldName.trim()}" multiselect`);
+ }
+ });
+});
+
+Then('I fill the {string} information with the following', (contactName: string, table: DataTable) => {
+ table.rows().forEach(row => {
+ const [fieldName, value, kind] = row;
+ if (kind === 'text') {
+ Step(this, `I type "${value}" into the "${fieldName.trim()}" form input for the "${contactName.trim()}"`);
+ } else if (kind === 'textreplace') {
+ Step(this, `I replace the "${fieldName.trim()}" with "${value}" form input for the "${contactName}"`);
+ } else if (kind === 'select') {
+ Step(this, `I select "${value}" from the "${fieldName.trim()}" form input for the "${contactName.trim()}"`);
+ } else if (kind === 'autocomplete') {
+ Step(this, `I type "${value}" and select "${value}" from the "${fieldName.trim()}" form autocomplete for the "${contactName.trim()}"`);
+ } else if (kind === 'multiselect') {
+ Step(this, `I select "${value}" from the "${fieldName.trim()}" multiselect for the "${contactName.trim()}"`);
+ }
+ });
+});
\ No newline at end of file
diff --git a/cypress/cypress/support/step_definitions/text_input_steps.ts b/cypress/cypress/support/step_definitions/text_input_steps.ts
new file mode 100644
index 0000000000..50d38e6a0a
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/text_input_steps.ts
@@ -0,0 +1,77 @@
+import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+
+let idir = true;
+
+BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () {
+ idir = false;
+});
+BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () {
+ idir = true;
+});
+
+/* Text Input Steps */
+
+Then('I type {string} into the {string} form input', (text: string, input: string) => {
+ cy.contains('label', input).then(($label) => {
+ cy.wrap($label.parent().parent())
+ .find('input[id*="input"]')
+ .then(($input) => {
+ cy.wrap($input)
+ .type(text)
+ .should('have.value', text)
+ .focus();
+ });
+ });
+});
+
+Then('I clear the {string} form input', (input: string) => {
+ cy.contains('label', input).then(($label) => {
+ cy.wrap($label.parent().parent())
+ .find('input[id*="input"]')
+ .clear();
+ });
+});
+
+Then(
+ 'I type {string} into the {string} form input for the {string}',
+ (value: string, fieldLabel: string, sectionTitle: string) => {
+
+ if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') {
+ cy.get('div.frame-01:first').within(() => {
+ Step(this, `I type "${value}" into the "${fieldLabel}" form input`);
+ });
+ } else {
+ cy.get(`[data-text="${sectionTitle}"]`).within(() => {
+ Step(this, `I type "${value}" into the "${fieldLabel}" form input`);
+ });
+ }
+});
+
+Then('I replace the {string} with {string} form input', (input: string, text: string) => {
+ cy.contains('label', input).then(($label) => {
+ cy.wrap($label.parent().parent())
+ .find('input[id*="input"]')
+ .then(($input) => {
+ cy.wrap($input)
+ .clear()
+ .type(text)
+ .should('have.value', text)
+ .focus();
+ });
+ });
+});
+
+Then(
+ 'I replace the {string} with {string} form input for the {string}',
+ (fieldLabel: string, value: string, sectionTitle: string) => {
+
+ if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') {
+ cy.get('div.frame-01:first').within(() => {
+ Step(this, `I replace the "${fieldLabel}" with "${value}" form input`);
+ });
+ } else {
+ cy.get(`[data-text="${sectionTitle}"]`).within(() => {
+ Step(this, `I replace the "${fieldLabel}" with "${value}" form input`);
+ });
+ }
+});
diff --git a/cypress/cypress/support/step_definitions/ui_ux_steps.ts b/cypress/cypress/support/step_definitions/ui_ux_steps.ts
new file mode 100644
index 0000000000..6655d32230
--- /dev/null
+++ b/cypress/cypress/support/step_definitions/ui_ux_steps.ts
@@ -0,0 +1,97 @@
+import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor";
+
+let idir = true;
+
+BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () {
+ idir = false;
+});
+BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () {
+ idir = true;
+});
+
+/* UX and UI checks */
+
+Then('The {string} component is using the font {string}', (component: string, font: string) => {
+ cy.contains('label', component).should('exist').then(($label) => {
+ const parentShadow = $label[0].getRootNode() as ShadowRoot;
+
+ if (parentShadow.host) {
+ const parentComponent = parentShadow.host as HTMLElement; // Host HTMLElement
+ cy.wrap(parentComponent).then((element) => {
+ checkComputedStyle(element, 'font-family', font); // Reuse the common function
+ });
+ } else {
+ throw new Error('No host element found for the given shadow root.');
+ }
+ });
+});
+
+Then('The {string} component is using the font {string} for the {string}', (component: string, font: string, scope: string) => {
+
+ if (scope === 'Primary location' || scope === 'Primary contact') {
+ cy.get('div.frame-01:first').within(() => {
+ Step(this, `The "${component}" component is using the font "${font}"`);
+ });
+ } else if(idir){
+ cy.get('cds-accordion cds-accordion-item')
+ .shadow()
+ .contains('div', scope).parent().parent().within(() => {
+ Step(this, `The "${component}" component is using the font "${font}"`);
+ });
+ } else {
+ cy.get(`div.frame-01[data-text="${scope}"]`).within(() => {
+ Step(this, `The "${component}" component is using the font "${font}"`);
+ });
+ }
+});
+
+Then('The {string} has weight {string}',(text: string, weight: string) => {
+ checkForCssProperty(text, 'font-weight', weight);
+});
+
+Then('The {string} has weight {string} inside the {string}',(text: string, weight: string, scope: string) => {
+ let computedScope = scope === 'form' ? 'div[role="main"]' : scope;
+ computedScope = scope === 'top' ? 'div[role="header"]' : computedScope;
+ computedScope = scope === 'bottom' ? 'div[role="footer"]' : computedScope;
+ checkForCssProperty(text, 'font-weight', weight, computedScope);
+});
+
+Then('The {string} size is {string}',(text: string, size: string) => {
+ checkForCssProperty(text, 'font-size', size);
+});
+
+Then('The {string} size is {string} inside the {string}',(text: string, size: string,scope: string) => {
+ let computedScope = scope === 'form' ? 'div[role="main"]' : scope;
+ computedScope = scope === 'top' ? 'div[role="header"]' : computedScope;
+ computedScope = scope === 'bottom' ? 'div[role="footer"]' : computedScope;
+ checkForCssProperty(text, 'font-size', size, computedScope);
+});
+
+// Helper functions to check computed CSS properties
+
+const checkComputedStyle = (
+ element: JQuery,
+ property: string,
+ expectedValue: string
+) => {
+ cy.window().then((win) => {
+ const computedStyle = win.getComputedStyle(element[0]); // Get the native DOM element
+ const styleValue = computedStyle.getPropertyValue(property); // Get the computed CSS property
+ // Assert that the style matches the expected value
+ expect(styleValue.trim().toLowerCase()).to.match(new RegExp(`^${expectedValue.toLowerCase()}`));
+ });
+};
+
+const checkForCssProperty = (text: string, property: string, value: string, scope: string = 'body') => {
+ cy.get(scope).find('*').each(($el) => {
+ cy.wrap($el).then((el) => {
+ const elementText = el.contents().filter(function () {
+ return this.nodeType === 3;
+ }).text().trim();
+
+ if (elementText === text) {
+ checkComputedStyle(el, property, value); // Reuse the common function
+ }
+ });
+ });
+};
diff --git a/frontend/src/components/grouping/ContactGroupComponent.vue b/frontend/src/components/grouping/ContactGroupComponent.vue
index 3700711460..e18edacd66 100644
--- a/frontend/src/components/grouping/ContactGroupComponent.vue
+++ b/frontend/src/components/grouping/ContactGroupComponent.vue
@@ -109,10 +109,18 @@ const logoutAndRedirect = () => {
window.open("https://www.bceid.ca/", "_blank", "noopener");
session?.logOut();
}
+
+const contactName = (contact: Contact, contactId: number) => {
+ if(contactId === 0) return "Primary contact";
+ if (!contact) return "Additional contact";
+ const name = `${contact.firstName} ${contact.lastName}`;
+ if (!name.trim()) return "Additional contact";
+ return name;
+};
-