/** * 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"); }); });