Skip to content

Commit c9bd340

Browse files
committed
test(vue): Add tests for Vue tracing mixins
1 parent b8dd290 commit c9bd340

File tree

1 file changed

+238
-0
lines changed

1 file changed

+238
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { getActiveSpan, startInactiveSpan } from '@sentry/browser';
2+
import type { Mock } from 'vitest';
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { DEFAULT_HOOKS } from '../../src/constants';
5+
import { createTracingMixins } from '../../src/tracing';
6+
7+
vi.mock('@sentry/browser', () => {
8+
return {
9+
getActiveSpan: vi.fn(),
10+
startInactiveSpan: vi.fn().mockImplementation(({ name, op }) => {
11+
return {
12+
end: vi.fn(),
13+
startChild: vi.fn(),
14+
name,
15+
op,
16+
};
17+
}),
18+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin',
19+
};
20+
});
21+
22+
vi.mock('../../src/vendor/components', () => {
23+
return {
24+
formatComponentName: vi.fn().mockImplementation(vm => {
25+
return vm.componentName || 'TestComponent';
26+
}),
27+
};
28+
});
29+
30+
const mockSpanFactory = (): { name?: string; op?: string; end: Mock; startChild: Mock } => ({
31+
name: undefined,
32+
op: undefined,
33+
end: vi.fn(),
34+
startChild: vi.fn(),
35+
});
36+
37+
vi.useFakeTimers();
38+
39+
describe('Vue Tracing Mixins', () => {
40+
let mockVueInstance: any;
41+
let mockRootInstance: any;
42+
43+
beforeEach(() => {
44+
vi.clearAllMocks();
45+
46+
mockRootInstance = {
47+
$root: null,
48+
componentName: 'RootComponent',
49+
$_sentrySpans: {},
50+
};
51+
mockRootInstance.$root = mockRootInstance; // Self-reference for root
52+
53+
mockVueInstance = {
54+
$root: mockRootInstance,
55+
componentName: 'TestComponent',
56+
$_sentrySpans: {},
57+
};
58+
59+
(getActiveSpan as any).mockReturnValue({ id: 'parent-span' });
60+
(startInactiveSpan as any).mockImplementation(({ name, op }: { name: string; op: string }) => {
61+
const newSpan = mockSpanFactory();
62+
newSpan.name = name;
63+
newSpan.op = op;
64+
return newSpan;
65+
});
66+
});
67+
68+
afterEach(() => {
69+
vi.clearAllTimers();
70+
});
71+
72+
describe('Mixin Creation', () => {
73+
it('should create mixins for default hooks', () => {
74+
const mixins = createTracingMixins();
75+
76+
DEFAULT_HOOKS.forEach(hook => {
77+
const hookPairs = {
78+
mount: ['beforeMount', 'mounted'],
79+
update: ['beforeUpdate', 'updated'],
80+
destroy: ['beforeDestroy', 'destroyed'],
81+
unmount: ['beforeUnmount', 'unmounted'],
82+
create: ['beforeCreate', 'created'],
83+
activate: ['activated', 'deactivated'],
84+
};
85+
86+
if (hook in hookPairs) {
87+
hookPairs[hook as keyof typeof hookPairs].forEach(lifecycleHook => {
88+
expect(mixins).toHaveProperty(lifecycleHook);
89+
// @ts-expect-error we check the type here
90+
expect(typeof mixins[lifecycleHook]).toBe('function');
91+
});
92+
}
93+
});
94+
});
95+
96+
it('should always include the activate and mount hooks', () => {
97+
const mixins = createTracingMixins({ hooks: undefined });
98+
99+
expect(Object.keys(mixins)).toEqual(['activated', 'deactivated', 'beforeMount', 'mounted']);
100+
});
101+
102+
it('should create mixins for custom hooks', () => {
103+
const mixins = createTracingMixins({ hooks: ['update'] });
104+
105+
expect(Object.keys(mixins)).toEqual([
106+
'beforeUpdate',
107+
'updated',
108+
'activated',
109+
'deactivated',
110+
'beforeMount',
111+
'mounted',
112+
]);
113+
});
114+
});
115+
116+
describe('Root Component Behavior', () => {
117+
it('should always create root span for root component regardless of tracking options', () => {
118+
const mixins = createTracingMixins({ trackComponents: false });
119+
120+
mixins.beforeMount.call(mockRootInstance);
121+
122+
expect(startInactiveSpan).toHaveBeenCalledWith(
123+
expect.objectContaining({
124+
name: 'Application Render',
125+
op: 'ui.vue.render',
126+
}),
127+
);
128+
});
129+
130+
it('should finish root span on timer after component spans end', () => {
131+
// todo/fixme: This root span is only finished if trackComponents is true --> it should probably be always finished
132+
const mixins = createTracingMixins({ trackComponents: true, timeout: 1000 });
133+
const rootMockSpan = mockSpanFactory();
134+
mockRootInstance.$_sentryRootSpan = rootMockSpan;
135+
136+
// Create and finish a component span
137+
mixins.beforeMount.call(mockVueInstance);
138+
mixins.mounted.call(mockVueInstance);
139+
140+
// Root span should not end immediately
141+
expect(rootMockSpan.end).not.toHaveBeenCalled();
142+
143+
// After timeout, root span should end
144+
vi.advanceTimersByTime(1001);
145+
expect(rootMockSpan.end).toHaveBeenCalled();
146+
});
147+
});
148+
149+
describe('Component Span Lifecycle', () => {
150+
it('should create and end spans correctly through lifecycle hooks', () => {
151+
const mixins = createTracingMixins({ trackComponents: true });
152+
153+
// 1. Create span in "before" hook
154+
mixins.beforeMount.call(mockVueInstance);
155+
156+
// Verify span was created with correct details
157+
expect(startInactiveSpan).toHaveBeenCalledWith(
158+
expect.objectContaining({
159+
name: 'Vue TestComponent',
160+
op: 'ui.vue.mount',
161+
}),
162+
);
163+
expect(mockVueInstance.$_sentrySpans.mount).toBeDefined();
164+
165+
// 2. Get the span for verification
166+
const componentSpan = mockVueInstance.$_sentrySpans.mount;
167+
168+
// 3. End span in "after" hook
169+
mixins.mounted.call(mockVueInstance);
170+
expect(componentSpan.end).toHaveBeenCalled();
171+
});
172+
173+
it('should clean up existing spans when creating new ones', () => {
174+
const mixins = createTracingMixins({ trackComponents: true });
175+
176+
// Create an existing span first
177+
const oldSpan = mockSpanFactory();
178+
mockVueInstance.$_sentrySpans.mount = oldSpan;
179+
180+
// Create a new span for the same operation
181+
mixins.beforeMount.call(mockVueInstance);
182+
183+
// Verify old span was ended and new span was created
184+
expect(oldSpan.end).toHaveBeenCalled();
185+
expect(mockVueInstance.$_sentrySpans.mount).not.toBe(oldSpan);
186+
});
187+
188+
it('should gracefully handle when "after" hook is called without "before" hook', () => {
189+
const mixins = createTracingMixins();
190+
191+
// Call mounted hook without calling beforeMount first
192+
expect(() => mixins.mounted.call(mockVueInstance)).not.toThrow();
193+
});
194+
195+
it('should skip spans when no active root span (transaction) exists', () => {
196+
const mixins = createTracingMixins({ trackComponents: true });
197+
198+
// Remove active spans
199+
(getActiveSpan as any).mockReturnValue(null);
200+
mockRootInstance.$_sentryRootSpan = null;
201+
202+
// Try to create a span
203+
mixins.beforeMount.call(mockVueInstance);
204+
205+
// No span should be created
206+
expect(startInactiveSpan).not.toHaveBeenCalled();
207+
});
208+
});
209+
210+
describe('Component Tracking Options', () => {
211+
it('should respect tracking configuration options', () => {
212+
// Test different tracking configurations with the same component
213+
const runTracingTest = (trackComponents: boolean | string[] | undefined, shouldTrack: boolean) => {
214+
vi.clearAllMocks();
215+
const mixins = createTracingMixins({ trackComponents });
216+
mixins.beforeMount.call(mockVueInstance);
217+
218+
if (shouldTrack) {
219+
expect(startInactiveSpan).toHaveBeenCalled();
220+
} else {
221+
expect(startInactiveSpan).not.toHaveBeenCalled();
222+
}
223+
};
224+
225+
// Test all tracking configurations
226+
runTracingTest(undefined, false); // Default - don't track
227+
runTracingTest(false, false); // Explicitly disabled
228+
runTracingTest(true, true); // Track all components
229+
runTracingTest(['TestComponent'], true); // Track by name (match)
230+
231+
// Test component not in tracking list
232+
vi.clearAllMocks();
233+
const mixins = createTracingMixins({ trackComponents: ['OtherComponent'] });
234+
mixins.beforeMount.call(mockVueInstance); // TestComponent
235+
expect(startInactiveSpan).not.toHaveBeenCalled();
236+
});
237+
});
238+
});

0 commit comments

Comments
 (0)