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(``)
+ })
+ )
+
+ 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(``)
+ })
+ )
+
+ 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(``)
+ })
+ )
+
+ 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(``)
- })
- )
-
- 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 {