/** * Live Tracking — PibiSection (flat TanStack Table redesign) * * Tests the approver flow for the per-measure PIBI request log rendered * directly on the DealPage (pibi-surveys tab), not in a drawer. * * 1. Empty state renders with a "Log first PIBI" prompt * 2. Flat table shows existing rows (no batch grouping) * 3. Every row has always-editable cells (measure select, date inputs) * 4. Save button is disabled when row is clean, enabled after editing * 5. Save calls PATCH for existing rows * 6. Delete calls DELETE * 7. "+ Add row" appends a blank row; save calls POST * 8. "Mark all complete" PATCHes all incomplete rows * 9. Scope badges appear for approved / proposed measures * * Requires LIVE_PORTFOLIO_SLUG; skipped otherwise. * All network calls are intercepted — no real DB / HubSpot round-trips. */ const PORTFOLIO_SLUG = Cypress.env("LIVE_PORTFOLIO_SLUG"); const PORTFOLIO_ID_GLOB = "*"; function stubGet(pibiRequests = []) { cy.intercept( "GET", `/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-requests*`, { body: { pibiRequests } }, ).as("getPibiRequests"); } function stubMeasures( approvedMeasures = ["ASHP", "CWI"], instructedMeasures = [], ) { cy.intercept( "GET", `/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-measures*`, { body: { pibiMeasures: approvedMeasures, approvedMeasures, instructedMeasures } }, ).as("getPibiMeasures"); } function stubPost(response = { ok: true, insertedCount: 1, hubspotSync: "ok" }) { cy.intercept( "POST", `/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-requests`, { body: response }, ).as("postPibiRequest"); } function stubPatch(id, response = { ok: true, hubspotSync: "ok" }) { cy.intercept( "PATCH", `/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-requests/${id}`, { body: response }, ).as(`patchPibiRequest-${id}`); } function stubDelete(id, response = { ok: true, hubspotSync: "ok" }) { cy.intercept( "DELETE", `/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-requests/${id}*`, { body: response }, ).as(`deletePibiRequest-${id}`); } function openDealPageAtPibiTab() { cy.visit(`/portfolio/${PORTFOLIO_SLUG}/your-projects/live`); cy.contains("button, [role=tab]", "Properties").click(); cy.get("[data-testid=property-row-link]").first().click(); cy.get("[data-testid=deal-page-tab-pibi-surveys]").click(); } describe("PibiSection", function () { before(function () { if (!PORTFOLIO_SLUG) { cy.log("LIVE_PORTFOLIO_SLUG not set — skipping PibiSection specs"); this.skip(); } }); beforeEach(() => { stubMeasures(); }); // ── Cycle 1: Empty state ────────────────────────────────────────────────── it("shows empty state with Log first PIBI prompt when no requests exist", () => { stubGet([]); openDealPageAtPibiTab(); cy.wait("@getPibiRequests"); cy.get("[data-testid=pibi-empty-state]").should("be.visible"); cy.get("[data-testid=pibi-empty-state]").should( "contain.text", "No PIBIs logged yet", ); cy.get("[data-testid=pibi-empty-add-row]").should("be.visible"); }); // ── Cycle 2: Flat table renders rows (no batch groups) ──────────────────── it("renders a flat table of rows with no batch grouping", () => { stubGet([ { id: "1", measureName: "ASHP", orderedAt: "2026-05-01T00:00:00.000Z", completedAt: null, }, { id: "2", measureName: "CWI", orderedAt: "2026-04-01T00:00:00.000Z", completedAt: null, }, ]); openDealPageAtPibiTab(); cy.wait("@getPibiRequests"); cy.get("[data-testid=pibi-row-1]").should("be.visible"); cy.get("[data-testid=pibi-row-2]").should("be.visible"); cy.get("[data-testid=pibi-batch-group]").should("not.exist"); }); // ── Cycle 3: Always-editable cells ──────────────────────────────────────── it("renders measure select and date inputs as always-editable cells", () => { stubGet([ { id: "5", measureName: "ASHP", orderedAt: "2026-05-01T00:00:00.000Z", completedAt: null, }, ]); openDealPageAtPibiTab(); cy.wait("@getPibiRequests"); cy.get("[data-testid=pibi-measure-select-5]").should("be.visible"); cy.get("[data-testid=pibi-ordered-date-5]").should("be.visible"); cy.get("[data-testid=pibi-completed-date-5]").should("be.visible"); }); // ── Cycle 4: Save disabled when clean, enabled after editing ────────────── it("shows Save disabled on load, enabled after editing a cell", () => { stubGet([ { id: "6", measureName: "ASHP", orderedAt: "2026-05-01T00:00:00.000Z", completedAt: null, }, ]); openDealPageAtPibiTab(); cy.wait("@getPibiRequests"); cy.get("[data-testid=pibi-save-6]").should("be.disabled"); cy.get("[data-testid=pibi-ordered-date-6]").clear().type("2026-06-01"); cy.get("[data-testid=pibi-save-6]").should("not.be.disabled"); }); // ── Cycle 5: Save calls PATCH ───────────────────────────────────────────── it("calls PATCH with updated fields when approver clicks Save", () => { stubGet([ { id: "10", measureName: "ASHP", orderedAt: "2026-05-01T00:00:00.000Z", completedAt: null, }, ]); stubPatch("10"); openDealPageAtPibiTab(); cy.wait("@getPibiRequests"); cy.get("[data-testid=pibi-completed-date-10]").type("2026-05-15"); cy.get("[data-testid=pibi-save-10]").click(); cy.wait("@patchPibiRequest-10").then((interception) => { expect(interception.request.body.dealId).to.be.a("string"); expect(interception.request.body.completedAt).to.include("2026-05-15"); }); }); // ── Cycle 6: Delete calls DELETE ────────────────────────────────────────── it("calls DELETE when approver clicks Delete on a row", () => { stubGet([ { id: "20", measureName: "EWI", orderedAt: "2026-05-01T00:00:00.000Z", completedAt: null, }, ]); stubDelete("20"); openDealPageAtPibiTab(); cy.wait("@getPibiRequests"); cy.get("[data-testid=pibi-delete-20]").click(); cy.wait("@deletePibiRequest-20"); }); // ── Cycle 7: Add row → POST ─────────────────────────────────────────────── it("appends a blank row on add-row click and POSTs on save", () => { stubGet([]); stubPost({ ok: true, insertedCount: 1, hubspotSync: "ok" }); cy.intercept("GET", `/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-requests*`, { body: { pibiRequests: [ { id: "99", measureName: "ASHP", orderedAt: new Date().toISOString(), completedAt: null, }, ], }, }).as("getPibiRequestsAfter"); openDealPageAtPibiTab(); cy.wait("@getPibiRequests"); cy.get("[data-testid=pibi-empty-add-row]").click(); cy.get("[data-testid=pibi-section]").find("tr[data-testid^=pibi-row-new]").should("have.length", 1); cy.get("[data-testid=pibi-section]") .find("[data-testid^=pibi-save-new]") .click(); cy.wait("@postPibiRequest").then((interception) => { expect(interception.request.body.measureNames).to.be.an("array").with.length(1); expect(interception.request.body.orderedAt).to.be.a("string"); }); }); // ── Cycle 8: Mark all complete ──────────────────────────────────────────── it("PATCHes all incomplete rows when Mark all complete is clicked", () => { stubGet([ { id: "30", measureName: "ASHP", orderedAt: "2026-05-01T00:00:00.000Z", completedAt: null, }, { id: "31", measureName: "CWI", orderedAt: "2026-05-01T00:00:00.000Z", completedAt: null, }, ]); stubPatch("30"); stubPatch("31"); openDealPageAtPibiTab(); cy.wait("@getPibiRequests"); cy.get("[data-testid=pibi-mark-all-complete]").click(); cy.wait("@patchPibiRequest-30").then((i) => { expect(i.request.body.completedAt).to.be.a("string"); }); cy.wait("@patchPibiRequest-31"); }); // ── Cycle 9: Scope badges ───────────────────────────────────────────────── it("shows Approved badge for a measure in approvedMeasures", () => { stubGet([ { id: "40", measureName: "ASHP", orderedAt: "2026-05-01T00:00:00.000Z", completedAt: null, }, ]); stubMeasures(["ASHP"], []); openDealPageAtPibiTab(); cy.wait("@getPibiRequests"); cy.get("[data-testid=pibi-row-40]").should("contain.text", "Approved"); }); });