Skip to content

Commit dd8aaa0

Browse files
committed
tests
1 parent c9a0eda commit dd8aaa0

4 files changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
5+
export default function ComponentAnnotationTestPage() {
6+
return (
7+
<div>
8+
<button
9+
id="annotated-btn"
10+
onClick={() => {
11+
Sentry.captureException(new Error('component-annotation-test'));
12+
}}
13+
>
14+
Click Me
15+
</button>
16+
</div>
17+
);
18+
}

dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ export default withSentryConfig(nextConfig, {
1111
_experimental: {
1212
vercelCronsMonitoring: true,
1313
turbopackApplicationKey: 'nextjs-16-e2e',
14+
turbopackReactComponentAnnotation: {
15+
enabled: true,
16+
},
1417
},
1518
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
const isWebpackDev = process.env.TEST_ENV === 'development-webpack';
5+
6+
test('React component annotation adds data-sentry-component attributes (Turbopack)', async ({ page }) => {
7+
test.skip(isWebpackDev, 'Only relevant for Turbopack builds');
8+
9+
await page.goto('/component-annotation');
10+
11+
const button = page.locator('#annotated-btn');
12+
await expect(button).toBeVisible();
13+
14+
// Set up error listener before clicking
15+
const errorPromise = waitForError('nextjs-16', errorEvent => {
16+
return errorEvent?.exception?.values?.some(value => value.value === 'component-annotation-test') ?? false;
17+
});
18+
19+
await button.click();
20+
const errorEvent = await errorPromise;
21+
22+
expect(errorEvent.exception?.values?.[0]?.value).toBe('component-annotation-test');
23+
24+
// In production, TEST_ENV=production is shared by both turbopack and webpack variants.
25+
// The component annotation loader only runs in Turbopack builds, so only assert
26+
// DOM attributes and breadcrumb component names when the build is actually turbopack.
27+
const annotatedEl = page.locator('[data-sentry-component="ComponentAnnotationTestPage"]');
28+
const isAnnotated = (await annotatedEl.count()) > 0;
29+
30+
if (isAnnotated) {
31+
await expect(annotatedEl).toBeVisible();
32+
33+
const clickBreadcrumb = errorEvent.breadcrumbs?.find(bc => bc.category === 'ui.click');
34+
expect(clickBreadcrumb?.data?.['ui.component_name']).toBe('ComponentAnnotationTestPage');
35+
}
36+
});

packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ vi.mock('path', async () => {
1717
if (lastArg === 'moduleMetadataInjectionLoader.js') {
1818
return '/mocked/path/to/moduleMetadataInjectionLoader.js';
1919
}
20+
if (lastArg === 'componentAnnotationLoader.js') {
21+
return '/mocked/path/to/componentAnnotationLoader.js';
22+
}
2023
return '/mocked/path/to/valueInjectionLoader.js';
2124
}),
2225
};
@@ -1080,6 +1083,182 @@ describe('moduleMetadataInjection with applicationKey', () => {
10801083
});
10811084
});
10821085

1086+
describe('componentAnnotation with turbopackReactComponentAnnotation', () => {
1087+
it('should add component annotation loader rule when enabled and Next.js >= 16', () => {
1088+
const pathResolveSpy = vi.spyOn(path, 'resolve');
1089+
pathResolveSpy.mockImplementation((...args: string[]) => {
1090+
const lastArg = args[args.length - 1];
1091+
if (lastArg === 'componentAnnotationLoader.js') {
1092+
return '/mocked/path/to/componentAnnotationLoader.js';
1093+
}
1094+
if (lastArg === 'moduleMetadataInjectionLoader.js') {
1095+
return '/mocked/path/to/moduleMetadataInjectionLoader.js';
1096+
}
1097+
return '/mocked/path/to/valueInjectionLoader.js';
1098+
});
1099+
1100+
const userNextConfig: NextConfigObject = {};
1101+
1102+
const result = constructTurbopackConfig({
1103+
userNextConfig,
1104+
userSentryOptions: {
1105+
_experimental: {
1106+
turbopackReactComponentAnnotation: { enabled: true },
1107+
},
1108+
},
1109+
nextJsVersion: '16.0.0',
1110+
});
1111+
1112+
expect(result.rules!['*.{tsx,jsx}']).toEqual({
1113+
condition: { not: 'foreign' },
1114+
loaders: [
1115+
{
1116+
loader: '/mocked/path/to/componentAnnotationLoader.js',
1117+
options: {
1118+
ignoredComponents: [],
1119+
},
1120+
},
1121+
],
1122+
});
1123+
});
1124+
1125+
it('should NOT add component annotation rule when enabled is false', () => {
1126+
const userNextConfig: NextConfigObject = {};
1127+
1128+
const result = constructTurbopackConfig({
1129+
userNextConfig,
1130+
userSentryOptions: {
1131+
_experimental: {
1132+
turbopackReactComponentAnnotation: { enabled: false },
1133+
},
1134+
},
1135+
nextJsVersion: '16.0.0',
1136+
});
1137+
1138+
expect(result.rules!['*.{tsx,jsx}']).toBeUndefined();
1139+
});
1140+
1141+
it('should NOT add component annotation rule when not set', () => {
1142+
const userNextConfig: NextConfigObject = {};
1143+
1144+
const result = constructTurbopackConfig({
1145+
userNextConfig,
1146+
userSentryOptions: {},
1147+
nextJsVersion: '16.0.0',
1148+
});
1149+
1150+
expect(result.rules!['*.{tsx,jsx}']).toBeUndefined();
1151+
});
1152+
1153+
it('should NOT add component annotation rule when Next.js < 16', () => {
1154+
const userNextConfig: NextConfigObject = {};
1155+
1156+
const result = constructTurbopackConfig({
1157+
userNextConfig,
1158+
userSentryOptions: {
1159+
_experimental: {
1160+
turbopackReactComponentAnnotation: { enabled: true },
1161+
},
1162+
},
1163+
nextJsVersion: '15.4.1',
1164+
});
1165+
1166+
expect(result.rules!['*.{tsx,jsx}']).toBeUndefined();
1167+
});
1168+
1169+
it('should NOT add component annotation rule when nextJsVersion is undefined', () => {
1170+
const userNextConfig: NextConfigObject = {};
1171+
1172+
const result = constructTurbopackConfig({
1173+
userNextConfig,
1174+
userSentryOptions: {
1175+
_experimental: {
1176+
turbopackReactComponentAnnotation: { enabled: true },
1177+
},
1178+
},
1179+
nextJsVersion: undefined,
1180+
});
1181+
1182+
expect(result.rules!['*.{tsx,jsx}']).toBeUndefined();
1183+
});
1184+
1185+
it('should pass ignoredComponents to loader options', () => {
1186+
const pathResolveSpy = vi.spyOn(path, 'resolve');
1187+
pathResolveSpy.mockImplementation((...args: string[]) => {
1188+
const lastArg = args[args.length - 1];
1189+
if (lastArg === 'componentAnnotationLoader.js') {
1190+
return '/mocked/path/to/componentAnnotationLoader.js';
1191+
}
1192+
if (lastArg === 'moduleMetadataInjectionLoader.js') {
1193+
return '/mocked/path/to/moduleMetadataInjectionLoader.js';
1194+
}
1195+
return '/mocked/path/to/valueInjectionLoader.js';
1196+
});
1197+
1198+
const userNextConfig: NextConfigObject = {};
1199+
1200+
const result = constructTurbopackConfig({
1201+
userNextConfig,
1202+
userSentryOptions: {
1203+
_experimental: {
1204+
turbopackReactComponentAnnotation: {
1205+
enabled: true,
1206+
ignoredComponents: ['Header', 'Footer'],
1207+
},
1208+
},
1209+
},
1210+
nextJsVersion: '16.0.0',
1211+
});
1212+
1213+
const rule = result.rules!['*.{tsx,jsx}'] as {
1214+
condition: unknown;
1215+
loaders: Array<{ loader: string; options: { ignoredComponents: string[] } }>;
1216+
};
1217+
expect(rule.loaders[0]!.options.ignoredComponents).toEqual(['Header', 'Footer']);
1218+
});
1219+
1220+
it('should coexist with value injection and module metadata rules', () => {
1221+
const pathResolveSpy = vi.spyOn(path, 'resolve');
1222+
pathResolveSpy.mockImplementation((...args: string[]) => {
1223+
const lastArg = args[args.length - 1];
1224+
if (lastArg === 'componentAnnotationLoader.js') {
1225+
return '/mocked/path/to/componentAnnotationLoader.js';
1226+
}
1227+
if (lastArg === 'moduleMetadataInjectionLoader.js') {
1228+
return '/mocked/path/to/moduleMetadataInjectionLoader.js';
1229+
}
1230+
return '/mocked/path/to/valueInjectionLoader.js';
1231+
});
1232+
1233+
const userNextConfig: NextConfigObject = {};
1234+
const mockRouteManifest: RouteManifest = {
1235+
dynamicRoutes: [],
1236+
staticRoutes: [{ path: '/', regex: '/' }],
1237+
isrRoutes: [],
1238+
};
1239+
1240+
const result = constructTurbopackConfig({
1241+
userNextConfig,
1242+
userSentryOptions: {
1243+
_experimental: {
1244+
turbopackApplicationKey: 'my-app',
1245+
turbopackReactComponentAnnotation: { enabled: true },
1246+
},
1247+
},
1248+
routeManifest: mockRouteManifest,
1249+
nextJsVersion: '16.0.0',
1250+
});
1251+
1252+
// Value injection rules should be present
1253+
expect(result.rules!['**/instrumentation-client.*']).toBeDefined();
1254+
expect(result.rules!['**/instrumentation.*']).toBeDefined();
1255+
// Module metadata loader should be present
1256+
expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toBeDefined();
1257+
// Component annotation loader should be present
1258+
expect(result.rules!['*.{tsx,jsx}']).toBeDefined();
1259+
});
1260+
});
1261+
10831262
describe('safelyAddTurbopackRule', () => {
10841263
const mockRule = {
10851264
loaders: [

0 commit comments

Comments
 (0)