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"