diff --git a/cypress/e2e/live-tracking/instruct-measure.cy.js b/cypress/e2e/live-tracking/instruct-measure.cy.js new file mode 100644 index 0000000..e633963 --- /dev/null +++ b/cypress/e2e/live-tracking/instruct-measure.cy.js @@ -0,0 +1,93 @@ +/** + * Live Tracking — Instruct measure flow (issue #253) + * + * Verifies the approver flow for instructing a measure that the + * coordinator did not propose: + * 1. the approver opens the property drawer at the Measures section, + * 2. picks a measure from the canonical catalogue dropdown and submits, + * 3. the drawer reflects the new instructed measure (optimistic chip), + * 4. the POST hits the instructed-measures route which pushes + * `instructed_measures` back to HubSpot, + * 5. the approval log surface shows a row for the new approval. + * + * Mirrors `halted-state.cy.js` / `domna-survey.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_INSTRUCT_DEAL_NAME"); +const INSTRUCT_MEASURE = "Loft insulation"; + +describe("Instruct measure — approver flow", function () { + before(function () { + if (!PORTFOLIO_SLUG) { + cy.log( + "LIVE_PORTFOLIO_SLUG env var not set — skipping live tracking specs", + ); + this.skip(); + } + }); + + function openDrawerForTargetDeal() { + cy.visit(`/portfolio/${PORTFOLIO_SLUG}/your-projects/live`); + + // Switch to the Measures tab — the easiest way into the drawer at the + // Measures section. + 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"); + cy.get("[data-testid=drawer-section-measures]").should("exist"); + } + + it("lets an approver instruct a measure and reflects it in the drawer + approval log", () => { + // Capture the API call so we can assert the payload that would be + // pushed to HubSpot under `instructed_measures`. + cy.intercept( + "POST", + `/api/portfolio/*/instructed-measures`, + ).as("instructMeasure"); + + openDrawerForTargetDeal(); + + // Approver-only form is visible at the bottom of the Measures section. + cy.get("[data-testid=instruct-measure-select]").should("be.visible"); + + cy.get("[data-testid=instruct-measure-select]").select(INSTRUCT_MEASURE); + cy.get("[data-testid=instruct-measure-submit]") + .should("not.be.disabled") + .click(); + + // Wait for the POST to land and assert the body shape that the + // service uses to drive the HubSpot push. + cy.wait("@instructMeasure").then((intercepted) => { + expect(intercepted.request.body).to.deep.include({ + measureName: INSTRUCT_MEASURE, + }); + // Response from our route signals the HubSpot sync outcome — it is + // either "ok" (mock recorded the push) or "failed" (network error). + // We accept either here so the spec stays portable across envs. + 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"); + }); + + // Drawer reflects the instructed measure as an optimistic chip. + cy.get("[data-testid=instructed-measures-list]").should("be.visible"); + cy.get("[data-testid=instructed-measure-chip]") + .should("contain.text", INSTRUCT_MEASURE); + + // No error banner. + cy.get("[data-testid=instruct-measure-error]").should("not.exist"); + + // Approval log section reveals the new approval row when expanded. + cy.contains("Approval Log").click(); + cy.contains(INSTRUCT_MEASURE).should("exist"); + }); +});