-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
test(vue): Add tests for Vue tracing mixins #16486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
import { getActiveSpan, startInactiveSpan } from '@sentry/browser'; | ||
import type { Mock } from 'vitest'; | ||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { DEFAULT_HOOKS } from '../../src/constants'; | ||
import { createTracingMixins } from '../../src/tracing'; | ||
|
||
vi.mock('@sentry/browser', () => { | ||
return { | ||
getActiveSpan: vi.fn(), | ||
startInactiveSpan: vi.fn().mockImplementation(({ name, op }) => { | ||
return { | ||
end: vi.fn(), | ||
startChild: vi.fn(), | ||
name, | ||
op, | ||
}; | ||
}), | ||
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin', | ||
}; | ||
}); | ||
|
||
vi.mock('../../src/vendor/components', () => { | ||
return { | ||
formatComponentName: vi.fn().mockImplementation(vm => { | ||
return vm.componentName || 'TestComponent'; | ||
}), | ||
}; | ||
}); | ||
|
||
const mockSpanFactory = (): { name?: string; op?: string; end: Mock; startChild: Mock } => ({ | ||
name: undefined, | ||
op: undefined, | ||
end: vi.fn(), | ||
startChild: vi.fn(), | ||
}); | ||
|
||
vi.useFakeTimers(); | ||
|
||
describe('Vue Tracing Mixins', () => { | ||
let mockVueInstance: any; | ||
let mockRootInstance: any; | ||
|
||
beforeEach(() => { | ||
vi.clearAllMocks(); | ||
|
||
mockRootInstance = { | ||
$root: null, | ||
componentName: 'RootComponent', | ||
$_sentrySpans: {}, | ||
}; | ||
mockRootInstance.$root = mockRootInstance; // Self-reference for root | ||
|
||
mockVueInstance = { | ||
$root: mockRootInstance, | ||
componentName: 'TestComponent', | ||
$_sentrySpans: {}, | ||
}; | ||
|
||
(getActiveSpan as any).mockReturnValue({ id: 'parent-span' }); | ||
(startInactiveSpan as any).mockImplementation(({ name, op }: { name: string; op: string }) => { | ||
const newSpan = mockSpanFactory(); | ||
newSpan.name = name; | ||
newSpan.op = op; | ||
return newSpan; | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
vi.clearAllTimers(); | ||
}); | ||
|
||
describe('Mixin Creation', () => { | ||
it('should create mixins for default hooks', () => { | ||
const mixins = createTracingMixins(); | ||
|
||
DEFAULT_HOOKS.forEach(hook => { | ||
const hookPairs = { | ||
mount: ['beforeMount', 'mounted'], | ||
update: ['beforeUpdate', 'updated'], | ||
destroy: ['beforeDestroy', 'destroyed'], | ||
unmount: ['beforeUnmount', 'unmounted'], | ||
create: ['beforeCreate', 'created'], | ||
activate: ['activated', 'deactivated'], | ||
}; | ||
|
||
if (hook in hookPairs) { | ||
hookPairs[hook as keyof typeof hookPairs].forEach(lifecycleHook => { | ||
expect(mixins).toHaveProperty(lifecycleHook); | ||
// @ts-expect-error we check the type here | ||
expect(typeof mixins[lifecycleHook]).toBe('function'); | ||
}); | ||
} | ||
}); | ||
}); | ||
|
||
it('should always include the activate and mount hooks', () => { | ||
const mixins = createTracingMixins({ hooks: undefined }); | ||
|
||
expect(Object.keys(mixins)).toEqual(['activated', 'deactivated', 'beforeMount', 'mounted']); | ||
}); | ||
|
||
it('should create mixins for custom hooks', () => { | ||
const mixins = createTracingMixins({ hooks: ['update'] }); | ||
|
||
expect(Object.keys(mixins)).toEqual([ | ||
'beforeUpdate', | ||
'updated', | ||
'activated', | ||
'deactivated', | ||
'beforeMount', | ||
'mounted', | ||
]); | ||
}); | ||
}); | ||
|
||
describe('Root Component Behavior', () => { | ||
it('should always create root span for root component regardless of tracking options', () => { | ||
const mixins = createTracingMixins({ trackComponents: false }); | ||
|
||
mixins.beforeMount.call(mockRootInstance); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q (for my understanding mostly): What happens if I call something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each operation (create, mount, ...) has its own independent span that is stored in the component's |
||
|
||
expect(startInactiveSpan).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
name: 'Application Render', | ||
op: 'ui.vue.render', | ||
}), | ||
); | ||
}); | ||
|
||
it('should finish root span on timer after component spans end', () => { | ||
// todo/fixme: This root span is only finished if trackComponents is true --> it should probably be always finished | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe someone knows more here about the wanted behavior 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm this looks like a bug then? according to this code There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it's always starting the span, but not ending it |
||
const mixins = createTracingMixins({ trackComponents: true, timeout: 1000 }); | ||
const rootMockSpan = mockSpanFactory(); | ||
mockRootInstance.$_sentryRootSpan = rootMockSpan; | ||
|
||
// Create and finish a component span | ||
mixins.beforeMount.call(mockVueInstance); | ||
mixins.mounted.call(mockVueInstance); | ||
|
||
// Root span should not end immediately | ||
expect(rootMockSpan.end).not.toHaveBeenCalled(); | ||
|
||
// After timeout, root span should end | ||
vi.advanceTimersByTime(1001); | ||
expect(rootMockSpan.end).toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('Component Span Lifecycle', () => { | ||
it('should create and end spans correctly through lifecycle hooks', () => { | ||
const mixins = createTracingMixins({ trackComponents: true }); | ||
|
||
// 1. Create span in "before" hook | ||
mixins.beforeMount.call(mockVueInstance); | ||
|
||
// Verify span was created with correct details | ||
expect(startInactiveSpan).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
name: 'Vue TestComponent', | ||
op: 'ui.vue.mount', | ||
}), | ||
); | ||
expect(mockVueInstance.$_sentrySpans.mount).toBeDefined(); | ||
|
||
// 2. Get the span for verification | ||
const componentSpan = mockVueInstance.$_sentrySpans.mount; | ||
|
||
// 3. End span in "after" hook | ||
mixins.mounted.call(mockVueInstance); | ||
expect(componentSpan.end).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should clean up existing spans when creating new ones', () => { | ||
const mixins = createTracingMixins({ trackComponents: true }); | ||
|
||
// Create an existing span first | ||
const oldSpan = mockSpanFactory(); | ||
mockVueInstance.$_sentrySpans.mount = oldSpan; | ||
|
||
// Create a new span for the same operation | ||
mixins.beforeMount.call(mockVueInstance); | ||
|
||
// Verify old span was ended and new span was created | ||
expect(oldSpan.end).toHaveBeenCalled(); | ||
expect(mockVueInstance.$_sentrySpans.mount).not.toBe(oldSpan); | ||
}); | ||
|
||
it('should gracefully handle when "after" hook is called without "before" hook', () => { | ||
const mixins = createTracingMixins(); | ||
|
||
// Call mounted hook without calling beforeMount first | ||
expect(() => mixins.mounted.call(mockVueInstance)).not.toThrow(); | ||
}); | ||
|
||
it('should skip spans when no active root span (transaction) exists', () => { | ||
const mixins = createTracingMixins({ trackComponents: true }); | ||
|
||
// Remove active spans | ||
(getActiveSpan as any).mockReturnValue(null); | ||
mockRootInstance.$_sentryRootSpan = null; | ||
|
||
// Try to create a span | ||
mixins.beforeMount.call(mockVueInstance); | ||
|
||
// No span should be created | ||
expect(startInactiveSpan).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('Component Tracking Options', () => { | ||
it('should respect tracking configuration options', () => { | ||
// Test different tracking configurations with the same component | ||
const runTracingTest = (trackComponents: boolean | string[] | undefined, shouldTrack: boolean) => { | ||
vi.clearAllMocks(); | ||
const mixins = createTracingMixins({ trackComponents }); | ||
mixins.beforeMount.call(mockVueInstance); | ||
|
||
if (shouldTrack) { | ||
expect(startInactiveSpan).toHaveBeenCalled(); | ||
} else { | ||
expect(startInactiveSpan).not.toHaveBeenCalled(); | ||
} | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. l: It feels like we're running multiple scenarios within one test here which I think we can better illustrate by using The two advantages of running individual tests here are:
happy to let you make the call on this, so feel free to disregard. Also maybe I missed a reason why the current form makes more sense. |
||
|
||
// Test all tracking configurations | ||
runTracingTest(undefined, false); // Default - don't track | ||
runTracingTest(false, false); // Explicitly disabled | ||
runTracingTest(true, true); // Track all components | ||
runTracingTest(['TestComponent'], true); // Track by name (match) | ||
|
||
// Test component not in tracking list | ||
vi.clearAllMocks(); | ||
const mixins = createTracingMixins({ trackComponents: ['OtherComponent'] }); | ||
mixins.beforeMount.call(mockVueInstance); // TestComponent | ||
expect(startInactiveSpan).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
l: Just to avoid confusion: We're talking about a span for the root component, right? Not to confuse with a root (i.e. parentless) span?