diff --git a/frontend/cypress/e2e/pages/SearchPage.cy.ts b/frontend/cypress/e2e/pages/SearchPage.cy.ts index e51aa6e908..d1c9e3ed95 100644 --- a/frontend/cypress/e2e/pages/SearchPage.cy.ts +++ b/frontend/cypress/e2e/pages/SearchPage.cy.ts @@ -5,21 +5,73 @@ describe("Search Page", () => { count: 0, }; - const checkDisplayedResults = (clientList: ClientSearchResult[]) => { - clientList.forEach((client) => { + const fullSearchCounter = { + count: 0, + }; + + const checkAutocompleteResults = (clientList: ClientSearchResult[]) => { + clientList.forEach((client, index) => { cy.get("#search-box") - .find(`cds-combo-box-item[data-value^="${client.clientNumber}"]`) - .should("exist"); + .find("cds-combo-box-item") + .eq(index) + .should("exist") + .should("have.attr", "data-id", client.clientNumber); }); }; + + const checkTableResults = (clientList: ClientSearchResult[]) => { + clientList.forEach((client, index) => { + cy.get("cds-table") + .find("cds-table-row") + .eq(index) + .contains(client.clientNumber) + .should("be.visible"); + + const acronymColumnIndex = 3; + + // only the first client has an acronym + const expectedValue = index === 0 ? client.clientAcronym : "-"; + + cy.get("cds-table") + .find("cds-table-row") + .eq(index) + .find(`cds-table-cell:nth-child(${acronymColumnIndex})`) + .contains(expectedValue) + .should("be.visible"); + }); + }; + beforeEach(() => { - // reset counter + // reset counters predictiveSearchCounter.count = 0; + fullSearchCounter.count = 0; + + cy.intercept( + { + pathname: "/api/clients/search", + query: { + keyword: "*", + }, + }, + (req) => { + predictiveSearchCounter.count++; + req.continue(); + }, + ).as("predictiveSearch"); - cy.intercept("/api/clients/search?keyword=*", (req) => { - predictiveSearchCounter.count++; - req.continue(); - }).as("predictiveSearch"); + cy.intercept( + { + pathname: "/api/clients/search", + query: { + page: "*", + size: "*", + }, + }, + (req) => { + fullSearchCounter.count++; + req.continue(); + }, + ).as("fullSearch"); cy.viewport(1920, 1080); cy.visit("/"); @@ -63,7 +115,7 @@ describe("Search Page", () => { .should("have.length", data.length) .should("be.visible"); - checkDisplayedResults(data); + checkAutocompleteResults(data); }); }); @@ -91,12 +143,12 @@ describe("Search Page", () => { .should("have.length", data.length) .should("be.visible"); - checkDisplayedResults(data); + checkAutocompleteResults(data); }); }); }); - describe("and user clicks a result", () => { + describe("and user clicks an Autocomplete result", () => { const clientNumber = "00054076"; beforeEach(() => { cy.get("#search-box") @@ -116,6 +168,136 @@ describe("Search Page", () => { ); }); }); + + describe("and clicks the Search button", () => { + beforeEach(() => { + cy.wait("@predictiveSearch"); + cy.get("#search-button").click(); + }); + it("makes one API call with the entered keywords", () => { + cy.wait("@fullSearch").then((interception) => { + const { query } = interception.request; + expect(query.keyword).to.eq("car"); + expect(query.page).to.eq("0"); + }); + + cy.wait(100); // Waits additional time to make sure there's no duplicate API calls. + cy.wrap(fullSearchCounter).its("count").should("eq", 1); + }); + + it("displays the results on the table", () => { + cy.wait("@fullSearch").then((interception) => { + const data = interception.response.body; + + cy.wrap(data).should("be.an", "array").and("have.length.greaterThan", 0); + + cy.get("cds-table") + .find("cds-table-row") + .should("have.length", data.length) + .should("be.visible"); + + checkTableResults(data); + }); + }); + + describe("and user clicks a result on the table", () => { + const clientNumber = "00191086"; + beforeEach(() => { + cy.get("cds-table").contains("cds-table-row", clientNumber).click(); + }); + it("navigates to the client details", () => { + const greenDomain = "green-domain.com"; + cy.get("@windowOpen").should( + "be.calledWith", + `https://${greenDomain}/int/client/client02MaintenanceAction.do?bean.clientNumber=${clientNumber}`, + "_blank", + "noopener", + ); + }); + }); + + describe("and clicks the Next page button on the table footer", () => { + beforeEach(() => { + cy.wait("@fullSearch"); + cy.get('[tooltip-text="Next page"]').click(); + }); + it("makes an API call for the second page of results", () => { + cy.wait("@fullSearch").then((interception) => { + const { query } = interception.request; + expect(query.keyword).to.eq("car"); + expect(query.page).to.eq("1"); + }); + cy.wrap(fullSearchCounter).its("count").should("eq", 2); + }); + + it("updates the results on the table", () => { + cy.wait("@fullSearch").then((interception) => { + const data = interception.response.body; + + cy.wrap(data).should("be.an", "array").and("have.length.greaterThan", 0); + + cy.get("cds-table") + .find("cds-table-row") + .should("have.length", data.length) + .should("be.visible"); + + checkTableResults(data); + }); + }); + + describe("and clicks the Search button again", () => { + beforeEach(() => { + cy.wait("@fullSearch"); + + // sanity check + cy.get("#pages-select").should("have.value", "2"); + + cy.get("#search-button").click(); + }); + it("makes a new API call for the first page of results", () => { + cy.wait("@fullSearch").then((interception) => { + const { query } = interception.request; + expect(query.keyword).to.eq("car"); + expect(query.page).to.eq("0"); + }); + cy.wrap(fullSearchCounter).its("count").should("eq", 3); + }); + + it("updates the results on the table", () => { + cy.wait("@fullSearch").then((interception) => { + // reset to page 1 + cy.get("#pages-select").should("have.value", "1"); + + const data = interception.response.body; + + cy.wrap(data).should("be.an", "array").and("have.length.greaterThan", 0); + + cy.get("cds-table") + .find("cds-table-row") + .should("have.length", data.length) + .should("be.visible"); + + checkTableResults(data); + }); + }); + }); + }); + }); + + describe("and hits enter on the search box", () => { + beforeEach(() => { + cy.wait("@predictiveSearch"); + cy.fillFormEntry("#search-box", "{enter}", { skipBlur: true }); + }); + it("makes the API call with the entered keywords", () => { + cy.wait("@fullSearch").then((interception) => { + const { query } = interception.request; + expect(query.keyword).to.eq("car"); + expect(query.page).to.eq("0"); + }); + cy.wrap(fullSearchCounter).its("count").should("eq", 1); + }); + }); }); describe("when user fills in the search box with an invalid value", () => { @@ -130,5 +312,69 @@ describe("Search Page", () => { cy.wait(500); // This time has to be greater than the debouncing time cy.wrap(predictiveSearchCounter).its("count").should("eq", 0); }); + + describe("and clicks the Search button", () => { + beforeEach(() => { + cy.get("#search-button").click(); + }); + it("makes no API call", () => { + cy.wait(500); // This time has to be greater than the debouncing time + cy.wrap(fullSearchCounter).its("count").should("eq", 0); + }); + }); + }); + + describe("Search with no keywords", () => { + beforeEach(() => { + cy.get("#search-button").click(); + }); + it("makes an API call even without any keywords", () => { + cy.wait("@fullSearch").then((interception) => { + const { query } = interception.request; + expect(query.keyword).to.eq(""); + expect(query.page).to.eq("0"); + }); + }); + }); + + describe("when the API is returning errors", () => { + beforeEach(() => { + // The "error" value actually triggers the error response + cy.fillFormEntry("#search-box", "error"); + }); + describe("and user clicks the Search button", () => { + beforeEach(() => { + cy.get("#search-button").click(); + }); + + it("displays an error notification", () => { + cy.wait("@fullSearch"); + + cy.get("cds-actionable-notification") + .shadow() + .contains("Something went wrong") + .should("be.visible"); + }); + + describe("and the API stops returning errors", () => { + beforeEach(() => { + cy.wait("@fullSearch"); + + // Replacing the "error" value actually triggers a successful response + cy.fillFormEntry("#search-box", "okay"); + }); + describe("and user clicks the Search button", () => { + beforeEach(() => { + cy.get("#search-button").click(); + }); + + it("hides the error notification", () => { + cy.wait("@fullSearch"); + + cy.get("cds-actionable-notification").should("not.exist"); + }); + }); + }); + }); }); }); diff --git a/frontend/src/assets/styles/global.scss b/frontend/src/assets/styles/global.scss index e5a2e01cf0..55b4dd72ca 100644 --- a/frontend/src/assets/styles/global.scss +++ b/frontend/src/assets/styles/global.scss @@ -1,8 +1,17 @@ @use "sass:map"; @use "@carbon/styles"; +@use '@carbon/styles/scss/theme'; +@use '@carbon/themes'; @use "@bcgov-nr/nr-fsa-theme/design-tokens/type-family.scss" as typeFamily; +@use '@bcgov-nr/nr-fsa-theme/design-tokens/light-tags.scss' as light-tag-overrides; + +// adds cds-tag related tokens +@include theme.add-component-tokens(light-tag-overrides.$light-tag-token-overrides); :root { + // the empty list passed in prevents from emitting the full theme + @include themes.theme(()); + --others-transparent-transparent: rgba(255, 255, 255, 0); --light-theme-border-subtle-01: #dfdfe1; --light-theme-button-button-primary: #0073e6; @@ -1486,6 +1495,22 @@ cds-table-header-cell { width: 1.5rem; } +.col-6_75rem { + width: 6.75rem; +} + +.col-19_4375rem { + width: 19.4375rem; +} + +.col-14_75rem { + width: 14.75rem; +} + +.col-7_0625rem { + width: 7.0625rem; +} + cds-pagination { border-radius: 0px 0px 4px 4px; border-top: 1px solid var(--light-theme-border-border-subtle-01, #dfdfe1); diff --git a/frontend/src/components/forms/AutoCompleteInputComponent.vue b/frontend/src/components/forms/AutoCompleteInputComponent.vue index 1f3075e400..a6a3833b83 100644 --- a/frontend/src/components/forms/AutoCompleteInputComponent.vue +++ b/frontend/src/components/forms/AutoCompleteInputComponent.vue @@ -42,6 +42,7 @@ const emit = defineEmits<{ (e: "update:model-value", value: string): void; (e: "update:selected-value", value: BusinessSearchResult | undefined): void; (e: "click:option", value: string): void; + (e: "press:enter"): void; }>(); //We initialize the error message handling for validation @@ -113,7 +114,7 @@ const emitValueChange = (newValue: string, isSelectEvent = false): void => { : undefined; } - emit("update:model-value", selectedValue?.name ?? newValue); + emit("update:model-value", selectedValue?.name ?? newValue ?? ""); emit("update:selected-value", selectedValue); emit("empty", isEmpty(newValue)); }; @@ -185,6 +186,9 @@ const validateInput = (newValue: string, validations = props.validations) => { } }; +const isClickSelectEvent = ref(false); +const isKeyboardSelectEvent = ref(false); + const selectAutocompleteItem = (event: any) => { const newValue = event?.detail?.item?.getAttribute("data-id"); emitValueChange(newValue, true); @@ -192,6 +196,13 @@ const selectAutocompleteItem = (event: any) => { }; const preSelectAutocompleteItem = (event: any) => { + if (!isClickSelectEvent.value) { + isKeyboardSelectEvent.value = true; + } + + // resets the flag + isClickSelectEvent.value = false; + if (event?.detail?.item) { const newValue = event?.detail?.item?.getAttribute("data-id"); emit("click:option", newValue); @@ -201,6 +212,20 @@ const preSelectAutocompleteItem = (event: any) => { } }; +const onPressEnter = () => { + // prevents emitting the event when this is a selection with the keyboard + if (!isKeyboardSelectEvent.value) { + emit("press:enter"); + } + + // resets the flag + isKeyboardSelectEvent.value = false; +}; + +const onItemClick = () => { + isClickSelectEvent.value = true; +}; + const onTyping = (event: any) => { isUserEvent.value = true; inputValue.value = event.srcElement._filterInputValue; @@ -314,6 +339,7 @@ const safeHelperText = computed(() => props.tip || " "); validateInput(event.srcElement._filterInputValue); } " + @keypress.enter="onPressEnter" :data-focus="id" :data-scroll="id" :data-id="'input-' + id" @@ -327,6 +353,7 @@ const safeHelperText = computed(() => props.tip || " "); :value="getComboBoxItemValue(item)" v-shadow :data-loading="item.name === loadingName" + @click="onItemClick" >