diff --git a/packages/svelte/jest.config.js b/packages/svelte/jest.config.js index cd02790794a7..63637952d9d6 100644 --- a/packages/svelte/jest.config.js +++ b/packages/svelte/jest.config.js @@ -3,4 +3,8 @@ const baseConfig = require('../../jest/jest.config.js'); module.exports = { ...baseConfig, testEnvironment: 'jsdom', + transform: { + '^.+\\.svelte$': 'svelte-jester', + ...baseConfig.transform, + }, }; diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0247f3ecf233..fba475b0aa6a 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -26,7 +26,9 @@ "svelte": "3.x" }, "devDependencies": { - "svelte": "3.49.0" + "@testing-library/svelte": "^3.2.1", + "svelte": "3.49.0", + "svelte-jester": "^2.3.2" }, "scripts": { "build": "run-p build:rollup build:types", diff --git a/packages/svelte/test/components/Dummy.svelte b/packages/svelte/test/components/Dummy.svelte new file mode 100644 index 000000000000..ef814473f6cf --- /dev/null +++ b/packages/svelte/test/components/Dummy.svelte @@ -0,0 +1,11 @@ + + +

Hi, I'm a dummy component for testing

diff --git a/packages/svelte/test/performance.test.ts b/packages/svelte/test/performance.test.ts new file mode 100644 index 000000000000..e5ecae861a41 --- /dev/null +++ b/packages/svelte/test/performance.test.ts @@ -0,0 +1,193 @@ +import { Scope } from '@sentry/hub'; +import { act, render } from '@testing-library/svelte'; + +// linter doesn't like Svelte component imports +// eslint-disable-next-line import/no-unresolved +import DummyComponent from './components/Dummy.svelte'; + +let returnUndefinedTransaction = false; + +const testTransaction: { spans: any[]; startChild: jest.Mock; finish: jest.Mock } = { + spans: [], + startChild: jest.fn(), + finish: jest.fn(), +}; +const testUpdateSpan = { finish: jest.fn() }; +const testInitSpan: any = { + transaction: testTransaction, + finish: jest.fn(), + startChild: jest.fn(), +}; + +jest.mock('@sentry/hub', () => { + const original = jest.requireActual('@sentry/hub'); + return { + ...original, + getCurrentHub(): { + getScope(): Scope; + } { + return { + getScope(): any { + return { + getTransaction: () => { + return returnUndefinedTransaction ? undefined : testTransaction; + }, + }; + }, + }; + }, + }; +}); + +describe('Sentry.trackComponent()', () => { + beforeEach(() => { + jest.resetAllMocks(); + testTransaction.spans = []; + + testTransaction.startChild.mockImplementation(spanCtx => { + testTransaction.spans.push(spanCtx); + return testInitSpan; + }); + + testInitSpan.startChild.mockImplementation((spanCtx: any) => { + testTransaction.spans.push(spanCtx); + return testUpdateSpan; + }); + + testInitSpan.finish = jest.fn(); + testInitSpan.endTimestamp = undefined; + returnUndefinedTransaction = false; + }); + + it('creates nested init and update spans on component initialization', () => { + render(DummyComponent, { props: { options: {} } }); + + expect(testTransaction.startChild).toHaveBeenCalledWith({ + description: '', + op: 'ui.svelte.init', + }); + + expect(testInitSpan.startChild).toHaveBeenCalledWith({ + description: '', + op: 'ui.svelte.update', + }); + + expect(testInitSpan.finish).toHaveBeenCalledTimes(1); + expect(testUpdateSpan.finish).toHaveBeenCalledTimes(1); + expect(testTransaction.spans.length).toEqual(2); + }); + + it('creates an update span, when the component is updated', async () => { + // Make the finish() function actually end the initSpan + testInitSpan.finish.mockImplementation(() => { + testInitSpan.endTimestamp = new Date().getTime(); + }); + + // first we create the component + const { component } = render(DummyComponent, { props: { options: {} } }); + + // then trigger an update + // (just changing the trackUpdates prop so that we trigger an update. # + // The value doesn't do anything here) + await act(() => component.$set({ options: { trackUpdates: true } })); + + // once for init (unimportant here), once for starting the update span + expect(testTransaction.startChild).toHaveBeenCalledTimes(2); + expect(testTransaction.startChild).toHaveBeenLastCalledWith({ + description: '', + op: 'ui.svelte.update', + }); + expect(testTransaction.spans.length).toEqual(3); + }); + + it('only creates init spans if trackUpdates is deactivated', () => { + render(DummyComponent, { props: { options: { trackUpdates: false } } }); + + expect(testTransaction.startChild).toHaveBeenCalledWith({ + description: '', + op: 'ui.svelte.init', + }); + + expect(testInitSpan.startChild).not.toHaveBeenCalled(); + + expect(testInitSpan.finish).toHaveBeenCalledTimes(1); + expect(testTransaction.spans.length).toEqual(1); + }); + + it('only creates update spans if trackInit is deactivated', () => { + render(DummyComponent, { props: { options: { trackInit: false } } }); + + expect(testTransaction.startChild).toHaveBeenCalledWith({ + description: '', + op: 'ui.svelte.update', + }); + + expect(testInitSpan.startChild).not.toHaveBeenCalled(); + + expect(testInitSpan.finish).toHaveBeenCalledTimes(1); + expect(testTransaction.spans.length).toEqual(1); + }); + + it('creates no spans if trackInit and trackUpdates are deactivated', () => { + render(DummyComponent, { props: { options: { trackInit: false, trackUpdates: false } } }); + + expect(testTransaction.startChild).not.toHaveBeenCalled(); + expect(testInitSpan.startChild).not.toHaveBeenCalled(); + expect(testTransaction.spans.length).toEqual(0); + }); + + it('sets a custom component name as a span description if `componentName` is provided', async () => { + render(DummyComponent, { + props: { options: { componentName: 'CustomComponentName' } }, + }); + + expect(testTransaction.startChild).toHaveBeenCalledWith({ + description: '', + op: 'ui.svelte.init', + }); + + expect(testInitSpan.startChild).toHaveBeenCalledWith({ + description: '', + op: 'ui.svelte.update', + }); + + expect(testInitSpan.finish).toHaveBeenCalledTimes(1); + expect(testUpdateSpan.finish).toHaveBeenCalledTimes(1); + expect(testTransaction.spans.length).toEqual(2); + }); + + it("doesn't do anything, if there's no ongoing transaction", async () => { + returnUndefinedTransaction = true; + + render(DummyComponent, { + props: { options: { componentName: 'CustomComponentName' } }, + }); + + expect(testInitSpan.finish).toHaveBeenCalledTimes(0); + expect(testUpdateSpan.finish).toHaveBeenCalledTimes(0); + expect(testTransaction.spans.length).toEqual(0); + }); + + it("doesn't record update spans, if there's no ongoing transaction at that time", async () => { + // Make the finish() function actually end the initSpan + testInitSpan.finish.mockImplementation(() => { + testInitSpan.endTimestamp = new Date().getTime(); + }); + + // first we create the component + const { component } = render(DummyComponent, { props: { options: {} } }); + + // then clear the current transaction and trigger an update + returnUndefinedTransaction = true; + await act(() => component.$set({ options: { trackUpdates: true } })); + + // we should only record the init spans (including the initial update) + // but not the second update + expect(testTransaction.startChild).toHaveBeenCalledTimes(1); + expect(testTransaction.startChild).toHaveBeenLastCalledWith({ + description: '', + op: 'ui.svelte.init', + }); + expect(testTransaction.spans.length).toEqual(2); + }); +}); diff --git a/scripts/test.ts b/scripts/test.ts index 582a67732b55..a4904abe50f6 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -17,6 +17,7 @@ const NODE_8_SKIP_TESTS_PACKAGES = [ '@sentry/nextjs', '@sentry/angular', '@sentry/remix', + '@sentry/svelte', // svelte testing library requires Node >= 10 ]; // We have to downgrade some of our dependencies in order to run tests in Node 8 and 10. diff --git a/yarn.lock b/yarn.lock index 7f25827379a4..544d08cb0d27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4979,6 +4979,20 @@ dependencies: defer-to-connect "^1.0.1" +"@testing-library/dom@^8.1.0": + version "8.17.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.17.1.tgz#2d7af4ff6dad8d837630fecd08835aee08320ad7" + integrity sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + "@testing-library/dom@^8.5.0": version "8.12.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.12.0.tgz#fef5e545533fb084175dda6509ee71d7d2f72e23" @@ -5013,6 +5027,13 @@ "@testing-library/dom" "^8.5.0" "@types/react-dom" "*" +"@testing-library/svelte@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@testing-library/svelte/-/svelte-3.2.1.tgz#c63bd2b7df7907f26e91b4ce0c50c77d8e7c4745" + integrity sha512-qP5nMAx78zt+a3y9Sws9BNQYP30cOQ/LXDYuAj7wNtw86b7AtB7TFAz6/Av9hFsW3IJHPBBIGff6utVNyq+F1g== + dependencies: + "@testing-library/dom" "^8.1.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -24945,6 +24966,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svelte-jester@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/svelte-jester/-/svelte-jester-2.3.2.tgz#9eb818da30807bbcc940b6130d15b2c34408d64f" + integrity sha512-JtxSz4FWAaCRBXbPsh4LcDs4Ua7zdXgLC0TZvT1R56hRV0dymmNP+abw67DTPF7sQPyNxWsOKd0Sl7Q8SnP8kg== + svelte@3.49.0: version "3.49.0" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"