@@ -8,24 +8,47 @@ function buildRoot(): HTMLDivElement {
88 return root ;
99}
1010
11- function buildFooterLink ( ) : HTMLAnchorElement {
11+ function buildFooterLink ( ) : HTMLElement {
12+ const footer = document . createElement ( "footer" ) ;
13+ footer . innerHTML = `
14+ <a
15+ class="text-muted"
16+ href="mailto:support@example.com"
17+ data-sentry-feedback-link=""
18+ >Contact Support</a>
19+ <span class="mx-2">·</span>
20+ ` ;
21+ document . body . appendChild ( footer ) ;
22+ return footer . querySelector ( "[data-sentry-feedback-link]" ) as HTMLElement ;
23+ }
24+
25+ function buildHiddenAnonymousFooterButton ( ) : HTMLElement {
1226 const footer = document . createElement ( "footer" ) ;
1327 footer . innerHTML = `
1428 <span class="d-none" data-sentry-feedback-footer="" data-sentry-feedback-hidden="true">
15- <span class="mx-2">·</span>
16- <a
17- class="text-muted"
18- href="#"
29+ <button
30+ type="button"
31+ class="btn btn-link text-muted p-0 border-0 align-baseline"
1932 data-sentry-feedback-link=""
20- data-sentry-feedback-hidden="true"
21- aria-hidden="true"
22- tabindex="-1"
23- >Report a bug</a>
33+ >Contact Support</button>
2434 </span>
25- <span class="mx-2">·</span>
2635 ` ;
2736 document . body . appendChild ( footer ) ;
28- return footer . querySelector ( "[data-sentry-feedback-link]" ) as HTMLAnchorElement ;
37+ return footer . querySelector ( "[data-sentry-feedback-link]" ) as HTMLElement ;
38+ }
39+
40+ function buildFeedbackShadowForm ( ) : ShadowRoot {
41+ const host = document . createElement ( "div" ) ;
42+ document . body . appendChild ( host ) ;
43+ const shadowRoot = host . attachShadow ( { mode : "open" } ) ;
44+ shadowRoot . innerHTML = `
45+ <div class="dialog__content">
46+ <form class="form">
47+ <fieldset class="form__right" data-sentry-feedback="true"></fieldset>
48+ </form>
49+ </div>
50+ ` ;
51+ return shadowRoot ;
2952}
3053
3154describe ( "attachSentryFeedbackTrigger" , ( ) => {
@@ -35,7 +58,7 @@ describe("attachSentryFeedbackTrigger", () => {
3558 vi . restoreAllMocks ( ) ;
3659 } ) ;
3760
38- it ( "attaches a report issue trigger on eligible roots and enables screenshots only when allowed " , ( ) => {
61+ it ( "repurposes the shared contact-support link on eligible roots and injects a modal fallback email link " , ( ) => {
3962 const attachTo = vi . fn ( ) ;
4063 vi . stubGlobal ( "window" , window ) ;
4164 vi . stubGlobal ( "Sentry" , {
@@ -47,7 +70,6 @@ describe("attachSentryFeedbackTrigger", () => {
4770
4871 const root = buildRoot ( ) ;
4972 const footerLink = buildFooterLink ( ) ;
50- const footerWrapper = document . querySelector ( "[data-sentry-feedback-footer]" ) as HTMLSpanElement ;
5173
5274 const attached = attachSentryFeedbackTrigger ( root , {
5375 allowScreenshot : true ,
@@ -56,23 +78,76 @@ describe("attachSentryFeedbackTrigger", () => {
5678
5779 expect ( attached ) . toBe ( true ) ;
5880 expect ( root . querySelector ( "[data-sentry-feedback-trigger]" ) ) . toBeNull ( ) ;
59- expect ( footerLink . textContent ) . toBe ( "Report a bug" ) ;
60- expect ( footerWrapper . classList . contains ( "d-none" ) ) . toBe ( false ) ;
61- expect ( footerWrapper . getAttribute ( "data-sentry-feedback-hidden" ) ) . toBe ( "false" ) ;
62- expect ( footerLink . getAttribute ( "data-sentry-feedback-hidden" ) ) . toBe ( "false" ) ;
63- expect ( footerLink . hasAttribute ( "aria-hidden" ) ) . toBe ( false ) ;
64- expect ( footerLink . hasAttribute ( "tabindex" ) ) . toBe ( false ) ;
81+ expect ( footerLink . textContent ) . toBe ( "Contact Support" ) ;
82+ expect ( footerLink . getAttribute ( "href" ) ) . toBe ( "mailto:support@example.com" ) ;
83+
84+ const clickEvent = new MouseEvent ( "click" , { bubbles : true , cancelable : true } ) ;
85+ footerLink . dispatchEvent ( clickEvent ) ;
86+ expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
87+
6588 expect ( attachTo ) . toHaveBeenCalledTimes ( 1 ) ;
6689 expect ( attachTo ) . toHaveBeenCalledWith (
6790 footerLink ,
6891 expect . objectContaining ( {
6992 enableScreenshot : true ,
93+ onFormOpen : expect . any ( Function ) ,
7094 showBranding : false ,
7195 tags : expect . objectContaining ( {
7296 feedback_surface : "groups-detail" ,
7397 } ) ,
7498 } ) ,
7599 ) ;
100+
101+ const attachOptions = attachTo . mock . calls [ 0 ] ?. [ 1 ] ;
102+ expect ( attachOptions ) . toBeDefined ( ) ;
103+
104+ const shadowRoot = buildFeedbackShadowForm ( ) ;
105+ attachOptions . onFormOpen ( ) ;
106+
107+ const fallback = shadowRoot . querySelector ( "[data-astra-support-fallback]" ) as HTMLParagraphElement | null ;
108+ expect ( fallback ) . not . toBeNull ( ) ;
109+ expect ( fallback ?. textContent ) . toContain ( "Prefer email?" ) ;
110+
111+ const fallbackLink = shadowRoot . querySelector ( "[data-astra-support-fallback-link]" ) as HTMLAnchorElement | null ;
112+ expect ( fallbackLink ?. textContent ) . toBe ( "Email support" ) ;
113+ expect ( fallbackLink ?. getAttribute ( "href" ) ) . toBe ( "mailto:support@example.com" ) ;
114+
115+ attachOptions . onFormOpen ( ) ;
116+ expect ( shadowRoot . querySelectorAll ( "[data-astra-support-fallback]" ) ) . toHaveLength ( 1 ) ;
117+ } ) ;
118+
119+ it ( "reveals the hidden anonymous contact-support scaffold only after feedback binds" , ( ) => {
120+ const attachTo = vi . fn ( ) ;
121+ ( window as typeof window & { Sentry ?: unknown } ) . Sentry = {
122+ getFeedback : ( ) => ( { attachTo } ) ,
123+ } ;
124+
125+ const root = buildRoot ( ) ;
126+ const footerLink = buildHiddenAnonymousFooterButton ( ) ;
127+ const footerWrapper = document . querySelector ( "[data-sentry-feedback-footer]" ) as HTMLElement ;
128+
129+ expect ( footerWrapper . classList . contains ( "d-none" ) ) . toBe ( true ) ;
130+ expect ( footerWrapper . getAttribute ( "data-sentry-feedback-hidden" ) ) . toBe ( "true" ) ;
131+
132+ const attached = attachSentryFeedbackTrigger ( root , {
133+ allowScreenshot : false ,
134+ surface : "anonymous-groups" ,
135+ } ) ;
136+
137+ expect ( attached ) . toBe ( true ) ;
138+ expect ( footerLink . textContent ) . toBe ( "Contact Support" ) ;
139+ expect ( footerLink . getAttribute ( "href" ) ) . toBeNull ( ) ;
140+ expect ( footerWrapper . classList . contains ( "d-none" ) ) . toBe ( false ) ;
141+ expect ( footerWrapper . getAttribute ( "data-sentry-feedback-hidden" ) ) . toBe ( "false" ) ;
142+ expect ( attachTo ) . toHaveBeenCalledWith (
143+ footerLink ,
144+ expect . objectContaining ( {
145+ enableScreenshot : false ,
146+ tags : expect . objectContaining ( {
147+ feedback_surface : "anonymous-groups" ,
148+ } ) ,
149+ } ) ,
150+ ) ;
76151 } ) ;
77152
78153 it ( "does not attach feedback on blocked roots" , ( ) => {
@@ -83,7 +158,6 @@ describe("attachSentryFeedbackTrigger", () => {
83158
84159 const root = buildRoot ( ) ;
85160 const footerLink = buildFooterLink ( ) ;
86- const footerWrapper = document . querySelector ( "[data-sentry-feedback-footer]" ) as HTMLSpanElement ;
87161 root . setAttribute ( "data-sentry-capture-disabled" , "" ) ;
88162
89163 const attached = attachSentryFeedbackTrigger ( root , {
@@ -93,9 +167,11 @@ describe("attachSentryFeedbackTrigger", () => {
93167
94168 expect ( attached ) . toBe ( false ) ;
95169 expect ( root . querySelector ( "[data-sentry-feedback-trigger]" ) ) . toBeNull ( ) ;
96- expect ( footerWrapper . classList . contains ( "d-none" ) ) . toBe ( true ) ;
97- expect ( footerWrapper . getAttribute ( "data-sentry-feedback-hidden" ) ) . toBe ( "true" ) ;
98- expect ( footerLink . getAttribute ( "data-sentry-feedback-hidden" ) ) . toBe ( "true" ) ;
170+ expect ( footerLink . getAttribute ( "href" ) ) . toBe ( "mailto:support@example.com" ) ;
171+
172+ const clickEvent = new MouseEvent ( "click" , { bubbles : true , cancelable : true } ) ;
173+ footerLink . dispatchEvent ( clickEvent ) ;
174+ expect ( clickEvent . defaultPrevented ) . toBe ( false ) ;
99175 expect ( attachTo ) . not . toHaveBeenCalled ( ) ;
100176 } ) ;
101177
@@ -111,7 +187,6 @@ describe("attachSentryFeedbackTrigger", () => {
111187
112188 const root = buildRoot ( ) ;
113189 const footerLink = buildFooterLink ( ) ;
114- const footerWrapper = document . querySelector ( "[data-sentry-feedback-footer]" ) as HTMLSpanElement ;
115190 blockedBoundary . appendChild ( root ) ;
116191
117192 const attached = attachSentryFeedbackTrigger ( root , {
@@ -121,8 +196,7 @@ describe("attachSentryFeedbackTrigger", () => {
121196
122197 expect ( attached ) . toBe ( false ) ;
123198 expect ( root . querySelector ( "[data-sentry-feedback-trigger]" ) ) . toBeNull ( ) ;
124- expect ( footerWrapper . classList . contains ( "d-none" ) ) . toBe ( true ) ;
125- expect ( footerLink . getAttribute ( "data-sentry-feedback-hidden" ) ) . toBe ( "true" ) ;
199+ expect ( footerLink . getAttribute ( "href" ) ) . toBe ( "mailto:support@example.com" ) ;
126200 expect ( attachTo ) . not . toHaveBeenCalled ( ) ;
127201 } ) ;
128202
0 commit comments