Skip to content

test(svelte): Add Svelte Testing Library and trackComponent tests #5686

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

Merged
merged 7 commits into from
Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/svelte/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ const baseConfig = require('../../jest/jest.config.js');
module.exports = {
...baseConfig,
testEnvironment: 'jsdom',
transform: {
'^.+\\.svelte$': 'svelte-jester',
...baseConfig.transform,
},
};
4 changes: 3 additions & 1 deletion packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions packages/svelte/test/components/Dummy.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
import { onMount, beforeUpdate, afterUpdate } from 'svelte';
import * as Sentry from '../../src/index';

// Pass options to trackComponent as props of this component
export let options;

Sentry.trackComponent(options);
</script>

<h1>Hi, I'm a dummy component for testing</h1>
193 changes: 193 additions & 0 deletions packages/svelte/test/performance.test.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes, Jest is a mystery to me. Apparently, mockImplementation (or mockReturnValue) must be called inside a describe or it/test function, while I can create empty jest.fn() mocks anywhere. I find this a little weird but whatever

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: '<Dummy>',
op: 'ui.svelte.init',
});

expect(testInitSpan.startChild).toHaveBeenCalledWith({
description: '<Dummy>',
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: '<Dummy>',
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: '<Dummy>',
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: '<Dummy>',
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: '<CustomComponentName>',
op: 'ui.svelte.init',
});

expect(testInitSpan.startChild).toHaveBeenCalledWith({
description: '<CustomComponentName>',
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: '<Dummy>',
op: 'ui.svelte.init',
});
expect(testTransaction.spans.length).toEqual(2);
});
});
1 change: 1 addition & 0 deletions scripts/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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==

[email protected]:
version "3.49.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
Expand Down