From ab7967c6bb8a4e4a701bb47bc50c2b7eb1205a9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:01:39 +0000 Subject: [PATCH 1/6] Initial plan From 018706f20421e02a73a9af7f2e29853f9d35dcbb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:05:20 +0000 Subject: [PATCH 2/6] feat(SectionCampaign): add customizable title-selector attribute Co-authored-by: timowestnosto <13622115+timowestnosto@users.noreply.github.com> --- .../SectionCampaign/SectionCampaign.ts | 5 +- .../SectionCampaign/SectionCampaign.spec.tsx | 53 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/components/SectionCampaign/SectionCampaign.ts b/src/components/SectionCampaign/SectionCampaign.ts index a5c31328..1050a3db 100644 --- a/src/components/SectionCampaign/SectionCampaign.ts +++ b/src/components/SectionCampaign/SectionCampaign.ts @@ -16,11 +16,13 @@ import { JSONResult } from "@nosto/nosto-js/client" * * @property {string} placement - The placement identifier for the campaign. * @property {string} section - The section to be used for Section Rendering API based rendering. + * @property {string} [titleSelector] - CSS selector for the title element to inject the campaign title. Defaults to ".nosto-title". */ @customElement("nosto-section-campaign") export class SectionCampaign extends NostoElement { @property(String) placement!: string @property(String) section!: string + @property(String) titleSelector?: string async connectedCallback() { this.toggleAttribute("loading", true) @@ -57,7 +59,8 @@ export class SectionCampaign extends NostoElement { const nostoSectionCampaign = doc.body.querySelector(`nosto-section-campaign[placement="${this.placement}"]`) const targetElement = nostoSectionCampaign || doc.body.firstElementChild if (rec.title && targetElement) { - const headingEl = targetElement.querySelector(".nosto-title") + const selector = this.titleSelector || ".nosto-title" + const headingEl = targetElement.querySelector(selector) if (headingEl) { headingEl.textContent = rec.title } diff --git a/test/components/SectionCampaign/SectionCampaign.spec.tsx b/test/components/SectionCampaign/SectionCampaign.spec.tsx index 0760ba43..e664c5d5 100644 --- a/test/components/SectionCampaign/SectionCampaign.spec.tsx +++ b/test/components/SectionCampaign/SectionCampaign.spec.tsx @@ -213,4 +213,57 @@ describe("SectionCampaign", () => { expect(attributeProductClicksInCampaign).toHaveBeenCalledWith(el, { products }) expect(el.hasAttribute("loading")).toBe(false) }) + + it("uses custom title-selector when provided", async () => { + const products = [{ handle: "product-a" }] + const { attributeProductClicksInCampaign, load } = mockNostoRecs({ + placement1: { products, title: "Custom Title" } + }) + + const sectionHTML = `

Default Title

Rendered Section
` + addHandlers( + http.get("/search", () => { + return HttpResponse.text(`
${sectionHTML}
`) + }) + ) + + const el = ( + + ) as SectionCampaign + document.body.appendChild(el) + + await el.connectedCallback() + + expect(load).toHaveBeenCalled() + expect(el.innerHTML).toContain("Custom Title") + expect(el.innerHTML).not.toContain("Default Title") + expect(attributeProductClicksInCampaign).toHaveBeenCalledWith(el, { products, title: "Custom Title" }) + expect(el.hasAttribute("loading")).toBe(false) + }) + + it("uses default .nosto-title selector when title-selector is not provided", async () => { + const products = [{ handle: "product-a" }] + const { attributeProductClicksInCampaign, load } = mockNostoRecs({ + placement1: { products, title: "Custom Title" } + }) + + const sectionHTML = `

Default Title

Should Not Change

` + addHandlers( + http.get("/search", () => { + return HttpResponse.text(`
${sectionHTML}
`) + }) + ) + + const el = () as SectionCampaign + document.body.appendChild(el) + + await el.connectedCallback() + + expect(load).toHaveBeenCalled() + expect(el.innerHTML).toContain("Custom Title") + expect(el.innerHTML).not.toContain("Default Title") + expect(el.innerHTML).toContain("Should Not Change") + expect(attributeProductClicksInCampaign).toHaveBeenCalledWith(el, { products, title: "Custom Title" }) + expect(el.hasAttribute("loading")).toBe(false) + }) }) From bd01354edecc6dfb223037f05493dc51b424eef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Wed, 18 Feb 2026 12:16:06 +0200 Subject: [PATCH 3/6] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/SectionCampaign/SectionCampaign.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SectionCampaign/SectionCampaign.ts b/src/components/SectionCampaign/SectionCampaign.ts index 1050a3db..4b0aaac9 100644 --- a/src/components/SectionCampaign/SectionCampaign.ts +++ b/src/components/SectionCampaign/SectionCampaign.ts @@ -16,7 +16,7 @@ import { JSONResult } from "@nosto/nosto-js/client" * * @property {string} placement - The placement identifier for the campaign. * @property {string} section - The section to be used for Section Rendering API based rendering. - * @property {string} [titleSelector] - CSS selector for the title element to inject the campaign title. Defaults to ".nosto-title". + * @property {string} [titleSelector] - CSS selector for the title element to inject the campaign title (attribute: "title-selector"). Defaults to ".nosto-title". */ @customElement("nosto-section-campaign") export class SectionCampaign extends NostoElement { From cfa6f8cb6b3c926441feb31b392952a95327face Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:01:48 +0000 Subject: [PATCH 4/6] refactor(SectionCampaign): require title-selector for title injection Co-authored-by: timowestnosto <13622115+timowestnosto@users.noreply.github.com> --- .../SectionCampaign/SectionCampaign.ts | 12 ++-- .../SectionCampaign/SectionCampaign.spec.tsx | 56 +++++++++++++++---- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/components/SectionCampaign/SectionCampaign.ts b/src/components/SectionCampaign/SectionCampaign.ts index 4b0aaac9..21ac73f2 100644 --- a/src/components/SectionCampaign/SectionCampaign.ts +++ b/src/components/SectionCampaign/SectionCampaign.ts @@ -16,7 +16,7 @@ import { JSONResult } from "@nosto/nosto-js/client" * * @property {string} placement - The placement identifier for the campaign. * @property {string} section - The section to be used for Section Rendering API based rendering. - * @property {string} [titleSelector] - CSS selector for the title element to inject the campaign title (attribute: "title-selector"). Defaults to ".nosto-title". + * @property {string} [titleSelector] - CSS selector for the title element to inject the campaign title (attribute: "title-selector"). If not provided, no title injection will be performed. */ @customElement("nosto-section-campaign") export class SectionCampaign extends NostoElement { @@ -58,9 +58,13 @@ export class SectionCampaign extends NostoElement { // Check if nosto-section-campaign element exists in the section body const nostoSectionCampaign = doc.body.querySelector(`nosto-section-campaign[placement="${this.placement}"]`) const targetElement = nostoSectionCampaign || doc.body.firstElementChild - if (rec.title && targetElement) { - const selector = this.titleSelector || ".nosto-title" - const headingEl = targetElement.querySelector(selector) + if (rec.title && targetElement && this.titleSelector) { + let headingEl: Element | null = null + try { + headingEl = targetElement.querySelector(this.titleSelector) + } catch (error) { + console.warn("[nosto-section-campaign] Invalid title selector:", this.titleSelector, error) + } if (headingEl) { headingEl.textContent = rec.title } diff --git a/test/components/SectionCampaign/SectionCampaign.spec.tsx b/test/components/SectionCampaign/SectionCampaign.spec.tsx index e664c5d5..06a791a4 100644 --- a/test/components/SectionCampaign/SectionCampaign.spec.tsx +++ b/test/components/SectionCampaign/SectionCampaign.spec.tsx @@ -1,5 +1,5 @@ /** @jsx createElement */ -import { describe, it, expect, Mock } from "vitest" +import { describe, it, expect, Mock, vi } from "vitest" import { SectionCampaign } from "@/components/SectionCampaign/SectionCampaign" import { RequestBuilder } from "@nosto/nosto-js/client" import { addHandlers } from "../../msw.setup" @@ -49,7 +49,7 @@ describe("SectionCampaign", () => { expect(el.hasAttribute("loading")).toBe(false) }) - it("replaces title in element with nosto-title attribute", async () => { + it("does not replace title when title-selector is not provided", async () => { const products = [{ handle: "product-a" }] const { attributeProductClicksInCampaign, load } = mockNostoRecs({ placement1: { products, title: "Custom Title" } @@ -68,8 +68,8 @@ describe("SectionCampaign", () => { await el.connectedCallback() expect(load).toHaveBeenCalled() - expect(el.innerHTML).toContain("Custom Title") - expect(el.innerHTML).not.toContain("Default Title") + expect(el.innerHTML).toContain("Default Title") + expect(el.innerHTML).not.toContain("Custom Title") expect(attributeProductClicksInCampaign).toHaveBeenCalledWith(el, { products, title: "Custom Title" }) expect(el.hasAttribute("loading")).toBe(false) }) @@ -144,7 +144,7 @@ describe("SectionCampaign", () => { expect(el.hasAttribute("loading")).toBe(false) }) - it("replaces title in nested nosto-section-campaign element with nosto-title class", async () => { + it("does not replace title in nested nosto-section-campaign when title-selector is not provided", async () => { const products = [{ handle: "product-a" }] const { attributeProductClicksInCampaign, load } = mockNostoRecs({ placement1: { products, title: "Custom Title" } @@ -163,8 +163,8 @@ describe("SectionCampaign", () => { await el.connectedCallback() expect(load).toHaveBeenCalled() - expect(el.innerHTML).toContain("Custom Title") - expect(el.innerHTML).not.toContain("Default Title") + expect(el.innerHTML).toContain("Default Title") + expect(el.innerHTML).not.toContain("Custom Title") expect(attributeProductClicksInCampaign).toHaveBeenCalledWith(el, { products, title: "Custom Title" }) expect(el.hasAttribute("loading")).toBe(false) }) @@ -241,7 +241,7 @@ describe("SectionCampaign", () => { expect(el.hasAttribute("loading")).toBe(false) }) - it("uses default .nosto-title selector when title-selector is not provided", async () => { + it("does not inject title when title-selector is not provided", async () => { const products = [{ handle: "product-a" }] const { attributeProductClicksInCampaign, load } = mockNostoRecs({ placement1: { products, title: "Custom Title" } @@ -260,10 +260,46 @@ describe("SectionCampaign", () => { await el.connectedCallback() expect(load).toHaveBeenCalled() - expect(el.innerHTML).toContain("Custom Title") - expect(el.innerHTML).not.toContain("Default Title") + expect(el.innerHTML).toContain("Default Title") + expect(el.innerHTML).not.toContain("Custom Title") expect(el.innerHTML).toContain("Should Not Change") expect(attributeProductClicksInCampaign).toHaveBeenCalledWith(el, { products, title: "Custom Title" }) expect(el.hasAttribute("loading")).toBe(false) }) + + it("handles invalid title-selector gracefully", async () => { + const products = [{ handle: "product-a" }] + const { attributeProductClicksInCampaign, load } = mockNostoRecs({ + placement1: { products, title: "Custom Title" } + }) + + const sectionHTML = `

Default Title

Rendered Section
` + addHandlers( + http.get("/search", () => { + return HttpResponse.text(`
${sectionHTML}
`) + }) + ) + + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + const el = ( + + ) as SectionCampaign + document.body.appendChild(el) + + await el.connectedCallback() + + expect(load).toHaveBeenCalled() + expect(el.innerHTML).toContain("Default Title") + expect(el.innerHTML).not.toContain("Custom Title") + expect(warnSpy).toHaveBeenCalledWith( + "[nosto-section-campaign] Invalid title selector:", + "[invalid::selector", + expect.any(DOMException) + ) + expect(attributeProductClicksInCampaign).toHaveBeenCalledWith(el, { products, title: "Custom Title" }) + expect(el.hasAttribute("loading")).toBe(false) + + warnSpy.mockRestore() + }) }) From 983b5f17d55efd70fbf1ce2a92905c77a3495bf6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:07:38 +0000 Subject: [PATCH 5/6] refactor(SectionCampaign): remove error handling for invalid selector Co-authored-by: timowestnosto <13622115+timowestnosto@users.noreply.github.com> --- .../SectionCampaign/SectionCampaign.ts | 7 +--- .../SectionCampaign/SectionCampaign.spec.tsx | 38 +------------------ 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/src/components/SectionCampaign/SectionCampaign.ts b/src/components/SectionCampaign/SectionCampaign.ts index 21ac73f2..f5d6ec47 100644 --- a/src/components/SectionCampaign/SectionCampaign.ts +++ b/src/components/SectionCampaign/SectionCampaign.ts @@ -59,12 +59,7 @@ export class SectionCampaign extends NostoElement { const nostoSectionCampaign = doc.body.querySelector(`nosto-section-campaign[placement="${this.placement}"]`) const targetElement = nostoSectionCampaign || doc.body.firstElementChild if (rec.title && targetElement && this.titleSelector) { - let headingEl: Element | null = null - try { - headingEl = targetElement.querySelector(this.titleSelector) - } catch (error) { - console.warn("[nosto-section-campaign] Invalid title selector:", this.titleSelector, error) - } + const headingEl = targetElement.querySelector(this.titleSelector) if (headingEl) { headingEl.textContent = rec.title } diff --git a/test/components/SectionCampaign/SectionCampaign.spec.tsx b/test/components/SectionCampaign/SectionCampaign.spec.tsx index 06a791a4..1cae2262 100644 --- a/test/components/SectionCampaign/SectionCampaign.spec.tsx +++ b/test/components/SectionCampaign/SectionCampaign.spec.tsx @@ -1,5 +1,5 @@ /** @jsx createElement */ -import { describe, it, expect, Mock, vi } from "vitest" +import { describe, it, expect, Mock } from "vitest" import { SectionCampaign } from "@/components/SectionCampaign/SectionCampaign" import { RequestBuilder } from "@nosto/nosto-js/client" import { addHandlers } from "../../msw.setup" @@ -266,40 +266,4 @@ describe("SectionCampaign", () => { expect(attributeProductClicksInCampaign).toHaveBeenCalledWith(el, { products, title: "Custom Title" }) expect(el.hasAttribute("loading")).toBe(false) }) - - it("handles invalid title-selector gracefully", async () => { - const products = [{ handle: "product-a" }] - const { attributeProductClicksInCampaign, load } = mockNostoRecs({ - placement1: { products, title: "Custom Title" } - }) - - const sectionHTML = `

Default Title

Rendered Section
` - addHandlers( - http.get("/search", () => { - return HttpResponse.text(`
${sectionHTML}
`) - }) - ) - - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) - - const el = ( - - ) as SectionCampaign - document.body.appendChild(el) - - await el.connectedCallback() - - expect(load).toHaveBeenCalled() - expect(el.innerHTML).toContain("Default Title") - expect(el.innerHTML).not.toContain("Custom Title") - expect(warnSpy).toHaveBeenCalledWith( - "[nosto-section-campaign] Invalid title selector:", - "[invalid::selector", - expect.any(DOMException) - ) - expect(attributeProductClicksInCampaign).toHaveBeenCalledWith(el, { products, title: "Custom Title" }) - expect(el.hasAttribute("loading")).toBe(false) - - warnSpy.mockRestore() - }) }) From 157041d776997eac3ef2b1149adb0f92c7e96ce9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:11:38 +0000 Subject: [PATCH 6/6] docs(SectionCampaign): simplify titleSelector JSDoc Co-authored-by: timowestnosto <13622115+timowestnosto@users.noreply.github.com> --- src/components/SectionCampaign/SectionCampaign.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SectionCampaign/SectionCampaign.ts b/src/components/SectionCampaign/SectionCampaign.ts index f5d6ec47..58242b9d 100644 --- a/src/components/SectionCampaign/SectionCampaign.ts +++ b/src/components/SectionCampaign/SectionCampaign.ts @@ -16,7 +16,7 @@ import { JSONResult } from "@nosto/nosto-js/client" * * @property {string} placement - The placement identifier for the campaign. * @property {string} section - The section to be used for Section Rendering API based rendering. - * @property {string} [titleSelector] - CSS selector for the title element to inject the campaign title (attribute: "title-selector"). If not provided, no title injection will be performed. + * @property {string} [titleSelector] - CSS selector for the title element to inject the campaign title (attribute: "title-selector"). */ @customElement("nosto-section-campaign") export class SectionCampaign extends NostoElement {