Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions e2e/tests/footer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,44 @@ describe("Footer", () => {

expect(hasLink).toBe(false);
});

it("leaves footers marked .cps-component-footer alone and anchors to the page footer", async () => {
await arrange({});
await act();

// A host app using <footer> inside a modal alongside the real page footer.
// The modal footer carries the opt-out class, so the shim must neither hide
// it nor anchor cps-global-footer next to it.
await page.evaluate(() => {
const modal = document.createElement("div");
modal.id = "host-modal";
const modalFooter = document.createElement("footer");
modalFooter.id = "host-modal-footer";
modalFooter.className = "cps-component-footer";
modalFooter.textContent = "modal footer";
modal.appendChild(modalFooter);
document.body.appendChild(modal);

const pageFooter = document.createElement("footer");
pageFooter.id = "host-page-footer";
pageFooter.textContent = "page footer";
document.body.appendChild(pageFooter);
});

// The page footer being hidden is our signal that the subscriber has run.
await page.waitForFunction(() => document.getElementById("host-page-footer")?.style.display === "none", { timeout: 5000, polling: 100 });

const result = await page.evaluate(() => {
const modalFooter = document.getElementById("host-modal-footer")!;
return {
modalFooterDisplay: window.getComputedStyle(modalFooter).display,
modalFooterObserved: !!modalFooter.dataset.cpsFooterEmailObserved,
globalFooterPrevSiblingId: document.querySelector("cps-global-footer")?.previousElementSibling?.id,
};
});

expect(result.modalFooterDisplay).not.toBe("none");
expect(result.modalFooterObserved).toBe(false);
expect(result.globalFooterPrevSiblingId).toBe("host-page-footer");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { DomMutationObserver } from "./DomMutationObserver";
const EMAIL_REGEX = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
const OBSERVED_MARKER = "cpsFooterEmailObserved";

const IGNORED_FOOTER_CLASS = "cps-component-footer";

// During a host SPA route transition the header is briefly hidden or
// zero-sized (display:none flash, parent collapsing, mid-paint state). The
// ResizeObserver faithfully fires with that tiny rect and — left unguarded —
Expand Down Expand Up @@ -97,7 +99,12 @@ export const footerSubscriber: DomMutationObserver = ({ preview, flags, config,
isActiveForContext: FEATURE_FLAGS.shouldShimFooter({ config, preview, flags }),
subscriptions: [
{
cssSelector: "footer",
// Host apps can use <footer> for things that aren't the page footer
// (sidebars, modals, cards). Agreed contract: host apps mark those with
// `.cps-component-footer` and we leave them alone. The class must be
// present when the element is rendered - if it's added later we'll
// already have hidden it and re-anchored cps-global-footer next to it.
cssSelector: `footer:not(.${IGNORED_FOOTER_CLASS})`,
handler: (element: HTMLElement) => {
let cpsGlobalFooter = element.ownerDocument.querySelector<HTMLCpsGlobalFooterElement>("cps-global-footer");
if (!cpsGlobalFooter) {
Expand Down
Loading