diff --git a/cypress/e2e/live-tracking/pibi-measures.cy.js b/cypress/e2e/live-tracking/pibi-measures.cy.js new file mode 100644 index 0000000..59afbe2 --- /dev/null +++ b/cypress/e2e/live-tracking/pibi-measures.cy.js @@ -0,0 +1,161 @@ +/** + * Live Tracking — PIBI measure selection flow (issue #254) + * + * Verifies the approver flow for selecting which measures on a deal go for + * PIBI: + * 1. the approver opens the property drawer at the PIBI section, + * 2. ticks and unticks measures in the multi-select, + * 3. saves the selection — the drawer reflects the ticked state, + * 4. the POST hits the pibi-measures route which pushes + * `measures_for_pibi_ordered` back to HubSpot. + * + * Mirrors `instruct-measure.cy.js`. The spec uses `cy.intercept` so the + * HubSpot push side-effect is observable without a real CRM round-trip. + */ + +const PORTFOLIO_SLUG = Cypress.env("LIVE_PORTFOLIO_SLUG"); +const TARGET_DEAL_NAME = Cypress.env("LIVE_PIBI_DEAL_NAME"); + +describe("PIBI measure selection — approver flow", function () { + before(function () { + if (!PORTFOLIO_SLUG) { + cy.log( + "LIVE_PORTFOLIO_SLUG env var not set — skipping live tracking specs", + ); + this.skip(); + } + }); + + function openDrawerAtPibiSection() { + cy.visit(`/portfolio/${PORTFOLIO_SLUG}/your-projects/live`); + + // Open a property row to get the detail drawer. + cy.contains("button, [role=tab]", "Measures").click(); + + if (TARGET_DEAL_NAME) { + cy.contains("[data-testid=measures-row]", TARGET_DEAL_NAME).click(); + } else { + cy.get("[data-testid=measures-row]").first().click(); + } + + cy.get("[data-testid=property-detail-drawer]").should("be.visible"); + + // Scroll to the PIBI section. + cy.get("[data-testid=drawer-section-pibi]").should("exist").scrollIntoView(); + } + + it("fetches the PIBI state and shows the multi-select for approvers", () => { + // Stub the GET so we control the initial state. + cy.intercept( + "GET", + `/api/portfolio/*/pibi-measures*`, + { + body: { + pibiMeasures: ["ASHP"], + approvedMeasures: ["ASHP", "Solar PV"], + instructedMeasures: [], + }, + }, + ).as("getPibiMeasures"); + + openDrawerAtPibiSection(); + + cy.wait("@getPibiMeasures"); + + // The multi-select should be visible for approvers. + cy.get("[data-testid=pibi-measure-selector]").should("be.visible"); + + // ASHP should be checked (was in pibiMeasures). + cy.get("[data-testid=pibi-measure-checkbox-ASHP]").should( + "be.checked", + ); + }); + + it("lets an approver tick/untick selections and POST to the route", () => { + // Stub GET to return a clean state. + cy.intercept( + "GET", + `/api/portfolio/*/pibi-measures*`, + { + body: { + pibiMeasures: [], + approvedMeasures: ["ASHP", "Solar PV"], + instructedMeasures: [], + }, + }, + ).as("getPibiMeasures"); + + // Intercept the POST so we can assert the body. + cy.intercept( + "POST", + `/api/portfolio/*/pibi-measures`, + ).as("savePibiMeasures"); + + openDrawerAtPibiSection(); + + cy.wait("@getPibiMeasures"); + + cy.get("[data-testid=pibi-measure-selector]").should("be.visible"); + + // Both approved measures (ASHP, Solar PV) should be pre-ticked since + // pibiMeasures was empty and approvedMeasures had values. + cy.get("[data-testid=pibi-measure-checkbox-ASHP]").should("be.checked"); + cy.get("[data-testid=pibi-measure-checkbox-Solar PV]").should("be.checked"); + + // Untick ASHP. + cy.get("[data-testid=pibi-measure-option-ASHP]").click(); + cy.get("[data-testid=pibi-measure-checkbox-ASHP]").should("not.be.checked"); + + // Save the selection. + cy.get("[data-testid=pibi-selector-save]").click(); + + cy.wait("@savePibiMeasures").then((intercepted) => { + // Body should reflect the new selection (Solar PV only). + expect(intercepted.request.body).to.have.property("dealId"); + expect(intercepted.request.body.measureNames).to.include("Solar PV"); + expect(intercepted.request.body.measureNames).not.to.include("ASHP"); + + // Route returns ok=true. + expect(intercepted.response.statusCode).to.be.oneOf([200, 201]); + expect(intercepted.response.body).to.have.property("ok", true); + expect(intercepted.response.body).to.have.property( + "hubspotSync", + ); + }); + + // No error banner visible. + cy.get("[data-testid=pibi-selector-error]").should("not.exist"); + }); + + it("pushes measures_for_pibi_ordered to HubSpot on a successful save", () => { + cy.intercept( + "GET", + `/api/portfolio/*/pibi-measures*`, + { + body: { + pibiMeasures: ["CWI"], + approvedMeasures: ["CWI"], + instructedMeasures: [], + }, + }, + ).as("getPibiMeasures"); + + // Stub the POST to confirm the property pushed. + cy.intercept("POST", `/api/portfolio/*/pibi-measures`, { + body: { ok: true, hubspotSync: "ok" }, + }).as("savePibiMeasures"); + + openDrawerAtPibiSection(); + + cy.wait("@getPibiMeasures"); + + cy.get("[data-testid=pibi-selector-save]").click(); + + cy.wait("@savePibiMeasures").then((intercepted) => { + // Confirm the POST body contains the right measures. + expect(intercepted.request.body).to.have.property("measureNames"); + // Response signals a successful HubSpot push. + expect(intercepted.response.body.hubspotSync).to.equal("ok"); + }); + }); +});