diff --git a/plugins/shortcuts/src/AddShortcut.test.tsx b/plugins/shortcuts/src/AddShortcut.test.tsx
new file mode 100644
index 0000000000..690bb2c59e
--- /dev/null
+++ b/plugins/shortcuts/src/AddShortcut.test.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { AddShortcut } from './AddShortcut';
+import { LocalStoredShortcuts } from './api';
+import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
+import { AlertDisplay } from '@backstage/core';
+
+describe('AddShortcut', () => {
+ const api = new LocalStoredShortcuts(MockStorageApi.create());
+
+ const props = {
+ onClose: jest.fn(),
+ anchorEl: document.createElement('div'),
+ api,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ document.title = 'some document title';
+ });
+
+ it('displays the title', async () => {
+ render(wrapInTestApp());
+
+ await waitFor(() => {
+ expect(screen.getByText('Add Shortcut')).toBeInTheDocument();
+ });
+ });
+
+ it('closes the popup', async () => {
+ render(wrapInTestApp());
+
+ fireEvent.click(screen.getByText('Cancel'));
+ await waitFor(() => {
+ expect(props.onClose).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('saves the input', async () => {
+ const spy = jest.spyOn(api, 'add');
+
+ render(wrapInTestApp());
+
+ const urlInput = screen.getByPlaceholderText('Enter a URL');
+ const titleInput = screen.getByPlaceholderText('Enter a display name');
+ fireEvent.change(urlInput, { target: { value: '/some-url' } });
+ fireEvent.change(titleInput, { target: { value: 'some title' } });
+
+ fireEvent.click(screen.getByText('Save'));
+ await waitFor(() => {
+ expect(spy).toBeCalledWith({
+ title: 'some title',
+ url: '/some-url',
+ });
+ });
+ });
+
+ it('pastes the values', async () => {
+ const spy = jest.spyOn(api, 'add');
+
+ render(
+ wrapInTestApp(, {
+ routeEntries: ['/some-initial-url'],
+ }),
+ );
+
+ fireEvent.click(screen.getByText('Paste Current Url'));
+ fireEvent.click(screen.getByText('Save'));
+ await waitFor(() => {
+ expect(spy).toBeCalledWith({
+ title: 'some document title',
+ url: '/some-initial-url',
+ });
+ });
+ });
+
+ it('displays errors', async () => {
+ jest.spyOn(api, 'add').mockRejectedValueOnce(new Error('some add error'));
+
+ render(
+ wrapInTestApp(
+ <>
+
+
+ >,
+ ),
+ );
+
+ fireEvent.click(screen.getByText('Paste Current Url'));
+ fireEvent.click(screen.getByText('Save'));
+ await waitFor(() => {
+ expect(
+ screen.getByText('Could not add shortcut: some add error'),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/shortcuts/src/EditShortcut.test.tsx b/plugins/shortcuts/src/EditShortcut.test.tsx
new file mode 100644
index 0000000000..bf2396e194
--- /dev/null
+++ b/plugins/shortcuts/src/EditShortcut.test.tsx
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { EditShortcut } from './EditShortcut';
+import { Shortcut } from './types';
+import { LocalStoredShortcuts } from './api';
+import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
+import { AlertDisplay } from '@backstage/core';
+
+describe('EditShortcut', () => {
+ const shortcut: Shortcut = {
+ id: 'id',
+ url: '/some-url',
+ title: 'some title',
+ };
+ const api = new LocalStoredShortcuts(MockStorageApi.create());
+
+ const props = {
+ onClose: jest.fn(),
+ anchorEl: document.createElement('div'),
+ shortcut,
+ api,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('displays the title', async () => {
+ render(wrapInTestApp());
+
+ await waitFor(() => {
+ expect(screen.getByText('Edit Shortcut')).toBeInTheDocument();
+ });
+ });
+
+ it('closes the popup', async () => {
+ render(wrapInTestApp());
+
+ fireEvent.click(screen.getByText('Cancel'));
+ await waitFor(() => {
+ expect(props.onClose).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('updates the shortcut', async () => {
+ const spy = jest.spyOn(api, 'update');
+
+ render(wrapInTestApp());
+
+ const urlInput = screen.getByPlaceholderText('Enter a URL');
+ const titleInput = screen.getByPlaceholderText('Enter a display name');
+ fireEvent.change(urlInput, { target: { value: '/some-new-url' } });
+ fireEvent.change(titleInput, { target: { value: 'some new title' } });
+
+ fireEvent.click(screen.getByText('Save'));
+ await waitFor(() => {
+ expect(spy).toBeCalledWith({
+ id: 'id',
+ title: 'some new title',
+ url: '/some-new-url',
+ });
+ expect(props.onClose).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('removes the shortcut', async () => {
+ const spy = jest.spyOn(api, 'remove');
+
+ render(wrapInTestApp());
+
+ fireEvent.click(screen.getByText('Remove'));
+ await waitFor(() => {
+ expect(spy).toBeCalledWith({
+ id: 'id',
+ title: 'some title',
+ url: '/some-url',
+ });
+ });
+ });
+
+ it('displays errors', async () => {
+ jest
+ .spyOn(api, 'update')
+ .mockRejectedValueOnce(new Error('some update error'));
+
+ jest
+ .spyOn(api, 'remove')
+ .mockRejectedValueOnce(new Error('some remove error'));
+
+ render(
+ wrapInTestApp(
+ <>
+
+
+ >,
+ ),
+ );
+
+ fireEvent.click(screen.getByText('Save'));
+ await waitFor(() => {
+ expect(
+ screen.getByText('Could not update shortcut: some update error'),
+ ).toBeInTheDocument();
+ });
+ fireEvent.click(screen.getByTestId('error-button-close'));
+
+ fireEvent.click(screen.getByText('Remove'));
+ await waitFor(() => {
+ expect(
+ screen.getByText('Could not delete shortcut: some remove error'),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/shortcuts/src/ShortcutForm.test.tsx b/plugins/shortcuts/src/ShortcutForm.test.tsx
new file mode 100644
index 0000000000..4586c484d0
--- /dev/null
+++ b/plugins/shortcuts/src/ShortcutForm.test.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { ShortcutForm } from './ShortcutForm';
+import { wrapInTestApp } from '@backstage/test-utils';
+
+describe('ShortcutForm', () => {
+ const props = {
+ onSave: jest.fn(),
+ onClose: jest.fn(),
+ };
+
+ it('displays validation messages', async () => {
+ render(wrapInTestApp());
+
+ const urlInput = screen.getByPlaceholderText('Enter a URL');
+ const titleInput = screen.getByPlaceholderText('Enter a display name');
+ fireEvent.change(urlInput, { target: { value: 'url' } });
+ fireEvent.change(titleInput, { target: { value: 't' } });
+
+ fireEvent.click(screen.getByText('Save'));
+ await waitFor(() => {
+ expect(
+ screen.getByText('Must be a relative URL (starts with a /)'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('Must be at least 2 characters'),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('calls the save handler', async () => {
+ render(
+ wrapInTestApp(
+ ,
+ ),
+ );
+
+ fireEvent.click(screen.getByText('Save'));
+ await waitFor(() => {
+ expect(props.onSave).toHaveBeenCalledWith(
+ expect.objectContaining({ title: 'some title', url: '/some-url' }),
+ expect.anything(),
+ );
+ });
+ });
+
+ it('calls the close handler', async () => {
+ render(wrapInTestApp());
+
+ fireEvent.click(screen.getByText('Cancel'));
+ await waitFor(() => {
+ expect(props.onClose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx
new file mode 100644
index 0000000000..219771909d
--- /dev/null
+++ b/plugins/shortcuts/src/ShortcutItem.test.tsx
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { ShortcutItem } from './ShortcutItem';
+import { Shortcut } from './types';
+import { SidebarContext } from '@backstage/core';
+import { LocalStoredShortcuts } from './api';
+import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
+import { pageTheme } from '@backstage/theme';
+
+describe('ShortcutItem', () => {
+ const shortcut: Shortcut = {
+ id: 'id',
+ url: '/some-url',
+ title: 'some title',
+ };
+ const api = new LocalStoredShortcuts(MockStorageApi.create());
+
+ it('displays the shortcut', async () => {
+ render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => {
+ expect(screen.getByText('ST')).toBeInTheDocument();
+ expect(screen.getByText('some title')).toBeInTheDocument();
+ });
+ });
+
+ it('calculates the shortcut text correctly', async () => {
+ const shortcut1: Shortcut = {
+ id: 'id1',
+ url: '/some-url',
+ title: 'onetitle',
+ };
+ const shortcut2: Shortcut = {
+ id: 'id2',
+ url: '/some-url',
+ title: 'two title',
+ };
+ const shortcut3: Shortcut = {
+ id: 'id3',
+ url: '/some-url',
+ title: 'more | title words',
+ };
+
+ const { rerender } = render(
+ wrapInTestApp(),
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('On')).toBeInTheDocument();
+ });
+
+ rerender(wrapInTestApp());
+ await waitFor(() => {
+ expect(screen.getByText('TT')).toBeInTheDocument();
+ });
+
+ rerender(wrapInTestApp());
+ await waitFor(() => {
+ expect(screen.getByText('MT')).toBeInTheDocument();
+ });
+ });
+
+ it('displays the edit icon on hover', async () => {
+ render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+
+ fireEvent.mouseOver(screen.getByText('ST'));
+ await waitFor(() => {
+ expect(screen.getByTestId('edit')).toBeInTheDocument();
+ });
+
+ fireEvent.mouseOut(screen.getByText('ST'));
+ await waitFor(() => {
+ expect(screen.queryByTestId('edit')).not.toBeInTheDocument();
+ });
+ });
+
+ it('gets the color based on the theme', async () => {
+ const { rerender } = render(
+ wrapInTestApp(),
+ );
+
+ await waitFor(() => {
+ expect(document.querySelector('circle')?.getAttribute('fill')).toEqual(
+ pageTheme.tool.colors[0],
+ );
+ });
+
+ const newShortcut: Shortcut = {
+ id: 'id',
+ url: '/catalog',
+ title: 'some title',
+ };
+ rerender(wrapInTestApp());
+
+ await waitFor(() => {
+ expect(document.querySelector('circle')?.getAttribute('fill')).toEqual(
+ pageTheme.home.colors[0],
+ );
+ });
+ });
+});
diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx
new file mode 100644
index 0000000000..088a3705d9
--- /dev/null
+++ b/plugins/shortcuts/src/Shortcuts.test.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { SidebarContext, ApiProvider, ApiRegistry } from '@backstage/core';
+import { wrapInTestApp, MockStorageApi } from '@backstage/test-utils';
+import { render, screen, waitFor } from '@testing-library/react';
+import { Shortcuts } from './Shortcuts';
+import { LocalStoredShortcuts, shortcutsApiRef } from './api';
+
+const apis = ApiRegistry.from([
+ [shortcutsApiRef, new LocalStoredShortcuts(MockStorageApi.create())],
+]);
+
+describe('Shortcuts', () => {
+ it('displays an add button', async () => {
+ render(
+ wrapInTestApp(
+
+
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !screen.queryByTestId('progress'));
+ expect(screen.getByText('Add Shortcuts')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts
new file mode 100644
index 0000000000..6fd45d73e5
--- /dev/null
+++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { MockStorageApi } from '@backstage/test-utils';
+import { pageTheme } from '@backstage/theme';
+import { Shortcut } from '../types';
+import { LocalStoredShortcuts } from './LocalStoredShortcuts';
+import { ShortcutApi } from './ShortcutApi';
+
+describe('LocalStoredShortcuts', () => {
+ // eslint-disable-next-line jest/no-done-callback
+ it('should observe shortcuts', async done => {
+ const shortcutApi: ShortcutApi = new LocalStoredShortcuts(
+ MockStorageApi.create(),
+ );
+ const shortcut: Shortcut = { id: 'id', title: 'title', url: '/url' };
+
+ await shortcutApi.add(shortcut);
+ shortcutApi.observe().subscribe(data => {
+ expect(data).toEqual(
+ expect.arrayContaining([{ ...shortcut, id: expect.anything() }]),
+ );
+ done();
+ });
+ });
+
+ it('should add shortcuts with ids', async () => {
+ const storageApi = MockStorageApi.create();
+ const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi);
+ const shortcut: Omit = { title: 'title', url: '/url' };
+ const spy = jest.spyOn(storageApi, 'set');
+
+ await shortcutApi.add(shortcut);
+ expect(spy).toHaveBeenCalledWith(
+ 'items',
+ expect.objectContaining([{ ...shortcut, id: expect.anything() }]),
+ );
+ });
+
+ it('should update shortcuts', async () => {
+ const storageApi = MockStorageApi.create();
+ const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi);
+ const shortcut: Shortcut = { id: 'someid', title: 'title', url: '/url' };
+ const spy = jest.spyOn(storageApi, 'set');
+
+ await shortcutApi.update(shortcut);
+ expect(spy).toHaveBeenCalledWith(
+ 'items',
+ expect.objectContaining([shortcut]),
+ );
+ });
+
+ it('should remove shortcuts', async () => {
+ const storageApi = MockStorageApi.create();
+ const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi);
+ const shortcut: Shortcut = { id: 'someid', title: 'title', url: '/url' };
+ const spy = jest.spyOn(storageApi, 'set');
+
+ await shortcutApi.remove(shortcut);
+ expect(spy).toHaveBeenCalledWith('items', []);
+ });
+
+ it('should get a color', () => {
+ const storageApi = MockStorageApi.create();
+ const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi);
+
+ expect(shortcutApi.getColor('/catalog')).toEqual(pageTheme.home.colors[0]);
+ });
+});