diff --git a/web/src/components/oncall/OnCallActivityTimeline.spec.ts b/web/src/components/oncall/OnCallActivityTimeline.spec.ts index b61e8a5844..2922931bc8 100644 --- a/web/src/components/oncall/OnCallActivityTimeline.spec.ts +++ b/web/src/components/oncall/OnCallActivityTimeline.spec.ts @@ -162,4 +162,39 @@ describe("OnCallActivityTimeline", () => { expect(text).toContain("14:37"); expect(text).toContain("14:39 · +2m"); }); + /// The agent's paragraph is held to three lines with a way to read the rest; generated lines are not. + it("clamps the AI verdict line and leaves the other lines whole", async () => { + const verdict: OnCallResponseEvent = { + kind: "ai_verdict", + at: OPENED + 300_000_000, + actor: "o2-sre", + body: "The alert fired because the test stream carries synthetic data. ".repeat(8), + }; + const wrapper = mount(OnCallActivityTimeline, { + props: { events: [...events, verdict], commentText: "", openedAt: OPENED }, + global: { plugins: [i18n, store] }, + }); + await flushPromises(); + + const clamped = wrapper.findAll('[data-test="oncall-clamped-text-body"]'); + expect(clamped).toHaveLength(1); + expect(clamped[0].text()).toContain("synthetic data"); + expect(clamped[0].classes()).toContain("line-clamp-3"); + }); + + /// The page scrolls past a long feed, so the note box stays stuck to its foot rather than below the fold. + it("sticks the note box to the page's foot when pinned", async () => { + const wrapper = mount(OnCallActivityTimeline, { + props: { events, commentText: "", openedAt: OPENED, pinComposer: true }, + global: { plugins: [i18n, store] }, + }); + await flushPromises(); + + const composer = wrapper + .find('[data-test="oncall-response-note-input"]') + .element.closest(".pt-2"); + expect(composer?.className).toContain("sticky"); + expect(composer?.className).toContain("bottom-0"); + expect(composer?.className).toContain("bg-surface-base"); + }); }); diff --git a/web/src/components/oncall/OnCallActivityTimeline.vue b/web/src/components/oncall/OnCallActivityTimeline.vue index 8c58a1245f..5e426e67a4 100644 --- a/web/src/components/oncall/OnCallActivityTimeline.vue +++ b/web/src/components/oncall/OnCallActivityTimeline.vue @@ -32,8 +32,7 @@ along with this program. If not, see . @pointerout="onPointerOut" > . data-test-comment-input="oncall-response-note-input" data-test-comment-send="oncall-response-note-submit" @submit="$emit('submit')" - /> + > + + . diff --git a/web/src/components/oncall/OnCallMyDeliveries.spec.ts b/web/src/components/oncall/OnCallMyDeliveries.spec.ts index 15c3bfed77..dd33088c5a 100644 --- a/web/src/components/oncall/OnCallMyDeliveries.spec.ts +++ b/web/src/components/oncall/OnCallMyDeliveries.spec.ts @@ -592,6 +592,97 @@ describe("OnCallMyDeliveries", () => { ); }); + describe("needs you pager", () => { + const open = (n: number) => + Array.from({ length: n }, (_, i) => + delivery({ event_id: `ev_${i}`, response_id: `resp_${i}`, at: AT + i }), + ); + const needsYou = (wrapper: ReturnType) => + wrapper.findComponent('[data-test="oncall-my-needs-you-table"]'); + + it("hides the pager while every open page fits", async () => { + answer(open(20)); + const wrapper = render(); + await flushPromises(); + + expect(needsYou(wrapper).attributes("pagination")).toBe("none"); + }); + + it("pages the open pages once they outgrow one page", async () => { + answer(open(21)); + const wrapper = render(); + await flushPromises(); + + expect(needsYou(wrapper).attributes("pagination")).toBe("client"); + expect(needsYou(wrapper).props("pageSize")).toBe(20); + }); + + it("keeps the chosen page size for the next visit", async () => { + answer(open(21)); + const wrapper = render(); + await flushPromises(); + needsYou(wrapper).vm.$emit("update:pageSize", 50); + await flushPromises(); + + const saved = useListViewState("oncall-my-deliveries", {}) as Record; + expect(saved.openPageSize).toBe(50); + }); + + it("pages through the open pages in the real table", async () => { + answer(open(25)); + const { OTable: _table, ...rest } = stubs; + const wrapper = mount(OnCallMyDeliveries, { + global: { plugins: [i18n, store], stubs: rest }, + }); + await flushPromises(); + + const card = wrapper.find('[data-test="oncall-my-needs-you"]'); + expect(card.find('[data-test="o2-table-pagination-bottom"]').exists()).toBe(true); + expect(card.find('[data-test="o2-table-pagination-info"]').text()).toMatch(/1\D+20\D+25/); + await card.find('[data-test="o2-table-next-page-btn"]').trigger("click"); + await flushPromises(); + expect(card.find('[data-test="o2-table-pagination-info"]').text()).toMatch(/21\D+25\D+25/); + }); + }); + + describe("history pager in the real table", () => { + const sent = (n: number) => + Array.from({ length: n }, (_, i) => + delivery({ + event_id: `ev_${i}`, + response_id: `resp_${i}`, + title: `alert ${i}`, + response_state: "resolved", + at: AT + i, + }), + ); + const { OTable: _table, ...realTable } = stubs; + const card = (wrapper: ReturnType) => + wrapper.find('[data-test="oncall-my-history"]'); + const rendered = (wrapper: ReturnType) => + card(wrapper).findAll('[data-test^="o2-table-row-"]:not([data-test$="drag-handle"])'); + + /// The pager arrives with the rows, so the table must not keep the row model it was created with. + it("renders one page of rows that matches the footer", async () => { + Object.assign(useListViewState("oncall-my-deliveries", {}), { pageSize: 10 }); + answer(sent(25)); + const wrapper = mount(OnCallMyDeliveries, { + global: { plugins: [i18n, store], stubs: realTable }, + }); + await flushPromises(); + await new Promise((resolve) => setTimeout(resolve, 0)); + await flushPromises(); + + expect(rendered(wrapper)).toHaveLength(10); + const info = () => card(wrapper).find('[data-test="o2-table-pagination-info"]').text(); + expect(info()).toMatch(/1\D+10\D+25/); + await card(wrapper).find('[data-test="o2-table-next-page-btn"]').trigger("click"); + await flushPromises(); + expect(rendered(wrapper)).toHaveLength(10); + expect(info()).toMatch(/11\D+20\D+25/); + }); + }); + describe("list view state", () => { const defaults = { unreadOnly: false, currentPage: 1, pageSize: 20 }; diff --git a/web/src/components/oncall/OnCallMyDeliveries.vue b/web/src/components/oncall/OnCallMyDeliveries.vue index 4fdcb349f3..42909f9e99 100644 --- a/web/src/components/oncall/OnCallMyDeliveries.vue +++ b/web/src/components/oncall/OnCallMyDeliveries.vue @@ -16,7 +16,10 @@ along with this program. If not, see .