Skip to content

Commit cef02b8

Browse files
authored
test(e2e): Add testing app for User Feedback (#18877)
1 parent d353444 commit cef02b8

27 files changed

+974
-0
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts
37+
38+
!*.d.ts
39+
40+
# Sentry
41+
.sentryclirc
42+
43+
.vscode
44+
45+
test-results
46+
event-dumps
47+
48+
.tmp_dev_server_logs
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
3+
public-hoist-pattern[]=*import-in-the-middle*
4+
public-hoist-pattern[]=*require-in-the-middle*
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Next.js 16 User Feedback E2E Tests
2+
3+
This test application verifies the Sentry User Feedback SDK functionality with Next.js 16.
4+
5+
## Tests
6+
7+
The tests cover various feedback APIs:
8+
9+
- `attachTo()` - Attaching feedback to custom buttons
10+
- `createWidget()` - Creating/removing feedback widget triggers
11+
- `createForm()` - Creating feedback forms with custom labels
12+
- `captureFeedback()` - Programmatic feedback submission
13+
- ThumbsUp/ThumbsDown sentiment tagging
14+
- Dialog cancellation
15+
16+
## Credits
17+
18+
Shoutout to [Ryan Albrecht](https://github.com/ryan953) for the underlying [testing app](https://github.com/ryan953/nextjs-test-feedback)!
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client';
2+
3+
import { useEffect, useState, useRef } from 'react';
4+
import * as Sentry from '@sentry/nextjs';
5+
6+
export default function AttachToFeedbackButton() {
7+
const [feedback, setFeedback] = useState<ReturnType<typeof Sentry.getFeedback>>();
8+
// Read `getFeedback` on the client only, to avoid hydration errors when server rendering
9+
useEffect(() => {
10+
setFeedback(Sentry.getFeedback());
11+
}, []);
12+
13+
const buttonRef = useRef<HTMLButtonElement>(null);
14+
useEffect(() => {
15+
if (feedback && buttonRef.current) {
16+
const unsubscribe = feedback.attachTo(buttonRef.current, {
17+
tags: { component: 'AttachToFeedbackButton' },
18+
onSubmitSuccess: data => {
19+
console.log('onSubmitSuccess', data);
20+
},
21+
});
22+
return unsubscribe;
23+
}
24+
return () => {};
25+
}, [feedback]);
26+
27+
return (
28+
<button
29+
className="hover:bg-hover px-4 py-2 rounded-md"
30+
type="button"
31+
ref={buttonRef}
32+
data-testid="attach-to-button"
33+
>
34+
Give me feedback (attachTo)
35+
</button>
36+
);
37+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
5+
export default function CrashReportButton() {
6+
return (
7+
<button
8+
className="hover:bg-hover px-4 py-2 rounded-md"
9+
type="button"
10+
data-testid="crash-report-button"
11+
onClick={() => {
12+
Sentry.captureException(new Error('Crash Report Button Clicked'), {
13+
data: { useCrashReport: true },
14+
});
15+
}}
16+
>
17+
Crash Report
18+
</button>
19+
);
20+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import * as Sentry from '@sentry/nextjs';
5+
6+
type FeedbackIntegration = ReturnType<typeof Sentry.getFeedback>;
7+
8+
export default function CreateFeedbackFormButton() {
9+
const [feedback, setFeedback] = useState<FeedbackIntegration>();
10+
// Read `getFeedback` on the client only, to avoid hydration errors when server rendering
11+
useEffect(() => {
12+
setFeedback(Sentry.getFeedback());
13+
}, []);
14+
15+
// Don't render custom feedback button if Feedback integration isn't installed
16+
if (!feedback) {
17+
return null;
18+
}
19+
20+
return (
21+
<button
22+
className="hover:bg-hover px-4 py-2 rounded-md"
23+
type="button"
24+
data-testid="create-form-button"
25+
onClick={async () => {
26+
const form = await feedback.createForm({
27+
tags: { component: 'CreateFeedbackFormButton' },
28+
});
29+
form.appendToDom();
30+
form.open();
31+
}}
32+
>
33+
Give me feedback (createForm)
34+
</button>
35+
);
36+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use client';
2+
3+
import type { RefObject } from 'react';
4+
import * as Sentry from '@sentry/nextjs';
5+
import { useEffect, useRef, useState } from 'react';
6+
7+
export default function FeedbackButton() {
8+
const buttonRef = useRef<HTMLButtonElement | null>(null);
9+
useFeedbackWidget({
10+
buttonRef,
11+
options: {
12+
tags: {
13+
component: 'FeedbackButton',
14+
},
15+
},
16+
});
17+
18+
return (
19+
<button ref={buttonRef} data-testid="feedback-button">
20+
Give Feedback
21+
</button>
22+
);
23+
}
24+
25+
function useFeedbackWidget({
26+
buttonRef,
27+
options = {},
28+
}: {
29+
buttonRef?: RefObject<HTMLButtonElement | null> | RefObject<HTMLAnchorElement | null>;
30+
options?: {
31+
tags?: Record<string, string>;
32+
};
33+
}) {
34+
const [feedback, setFeedback] = useState<ReturnType<typeof Sentry.getFeedback>>();
35+
// Read `getFeedback` on the client only, to avoid hydration errors when server rendering
36+
useEffect(() => {
37+
setFeedback(Sentry.getFeedback());
38+
}, []);
39+
40+
useEffect(() => {
41+
if (!feedback) {
42+
return undefined;
43+
}
44+
45+
if (buttonRef) {
46+
if (buttonRef.current) {
47+
return feedback.attachTo(buttonRef.current, options);
48+
}
49+
} else {
50+
const widget = feedback.createWidget(options);
51+
return () => {
52+
widget.removeFromDom();
53+
};
54+
}
55+
56+
return undefined;
57+
}, [buttonRef, feedback, options]);
58+
59+
return feedback;
60+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
5+
export default function MyFeedbackForm() {
6+
return (
7+
<form
8+
id="my-feedback-form"
9+
data-testid="my-feedback-form"
10+
onSubmit={async event => {
11+
event.preventDefault();
12+
const formData = new FormData(event.currentTarget);
13+
14+
const attachment = async () => {
15+
const attachmentField = formData.get('attachment') as File;
16+
if (!attachmentField || attachmentField.size === 0) {
17+
return null;
18+
}
19+
const data = new Uint8Array(await attachmentField.arrayBuffer());
20+
const attachmentData = {
21+
data,
22+
filename: 'upload',
23+
};
24+
return attachmentData;
25+
};
26+
27+
Sentry.getCurrentScope().setTags({ component: 'MyFeedbackForm' });
28+
const attachmentData = await attachment();
29+
Sentry.captureFeedback(
30+
{
31+
name: String(formData.get('name')),
32+
email: String(formData.get('email')),
33+
message: String(formData.get('message')),
34+
},
35+
attachmentData ? { attachments: [attachmentData] } : undefined,
36+
);
37+
}}
38+
>
39+
<input name="name" placeholder="Your Name" data-testid="my-form-name" />
40+
<input name="email" placeholder="Your Email" data-testid="my-form-email" />
41+
<textarea name="message" placeholder="What's the issue?" data-testid="my-form-message" />
42+
<input type="file" name="attachment" data-testid="my-form-attachment" />
43+
<button type="submit" data-testid="my-form-submit">
44+
Submit
45+
</button>
46+
</form>
47+
);
48+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
import { Fragment, useEffect, useState } from 'react';
5+
6+
export default function ThumbsUpDownButtons() {
7+
const [feedback, setFeedback] = useState<ReturnType<typeof Sentry.getFeedback>>();
8+
// Read `getFeedback` on the client only, to avoid hydration errors when server rendering
9+
useEffect(() => {
10+
setFeedback(Sentry.getFeedback());
11+
}, []);
12+
13+
return (
14+
<Fragment>
15+
<strong>Was this helpful?</strong>
16+
<button
17+
title="I like this"
18+
data-testid="thumbs-up-button"
19+
onClick={async () => {
20+
const form = await feedback?.createForm({
21+
messagePlaceholder: 'What did you like most?',
22+
tags: {
23+
component: 'ThumbsUpDownButtons',
24+
'feedback.type': 'positive',
25+
},
26+
});
27+
form?.appendToDom();
28+
form?.open();
29+
}}
30+
>
31+
Yes
32+
</button>
33+
34+
<button
35+
title="I don't like this"
36+
data-testid="thumbs-down-button"
37+
onClick={async () => {
38+
const form = await feedback?.createForm({
39+
messagePlaceholder: 'How can we improve?',
40+
tags: {
41+
component: 'ThumbsUpDownButtons',
42+
'feedback.type': 'negative',
43+
},
44+
});
45+
form?.appendToDom();
46+
form?.open();
47+
}}
48+
>
49+
No
50+
</button>
51+
</Fragment>
52+
);
53+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import * as Sentry from '@sentry/nextjs';
5+
6+
export default function ToggleFeedbackButton() {
7+
const [feedback, setFeedback] = useState<ReturnType<typeof Sentry.getFeedback>>();
8+
// Read `getFeedback` on the client only, to avoid hydration errors when server rendering
9+
useEffect(() => {
10+
setFeedback(Sentry.getFeedback());
11+
}, []);
12+
13+
const [widget, setWidget] = useState<null | { removeFromDom: () => void }>();
14+
return (
15+
<button
16+
className="hover:bg-hover px-4 py-2 rounded-md"
17+
type="button"
18+
data-testid="toggle-feedback-button"
19+
onClick={async () => {
20+
if (widget) {
21+
widget.removeFromDom();
22+
setWidget(null);
23+
} else if (feedback) {
24+
setWidget(
25+
feedback.createWidget({
26+
tags: { component: 'ToggleFeedbackButton' },
27+
}),
28+
);
29+
}
30+
}}
31+
>
32+
{widget ? 'Remove Widget' : 'Create Widget'}
33+
</button>
34+
);
35+
}

0 commit comments

Comments
 (0)