// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2022 The Pybricks Authors
import { Classes } from '@blueprintjs/core';
import { cleanup, fireEvent, waitFor } from '@testing-library/react';
import * as monaco from 'monaco-editor';
import React from 'react';
import { testRender, uuid } from '../../test';
import { FileMetadata } from '../fileStorage';
import { useFileStorageMetadata, useFileStoragePath } from '../fileStorage/hooks';
import { defined } from '../utils';
import Editor from './Editor';
import { editorActivateFile, editorCloseFile } from './actions';
const testFile: FileMetadata = {
uuid: uuid(0),
path: 'test.file',
sha256: '',
viewState: null,
};
jest.setTimeout(1000000);
afterEach(() => {
cleanup();
jest.resetAllMocks();
localStorage.clear();
sessionStorage.clear();
});
describe('Editor', () => {
describe('tabs', () => {
it('should dispatch activate action when tab is clicked', async () => {
jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]);
jest.mocked(useFileStoragePath).mockReturnValue(testFile.path);
const [user, editor, dispatch] = testRender(, {
editor: { openFileUuids: [testFile.uuid] },
});
await user.click(editor.getByRole('tab', { name: 'test.file' }));
expect(dispatch).toHaveBeenCalledWith(editorActivateFile(testFile.uuid));
});
it.each(['{Enter}', '{Space}'])(
'should dispatch activate action when %s key is pressed',
async (key) => {
jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]);
jest.mocked(useFileStoragePath).mockReturnValue(testFile.path);
const [user, editor, dispatch] = testRender(, {
editor: { openFileUuids: [testFile.uuid] },
});
await user.type(editor.getByRole('tab', { name: 'test.file' }), key);
expect(dispatch).toHaveBeenCalledWith(
editorActivateFile(testFile.uuid),
);
},
);
it('should dispatch close action when close button is clicked', async () => {
jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]);
jest.mocked(useFileStoragePath).mockReturnValue(testFile.path);
const [user, editor, dispatch] = testRender(, {
editor: { openFileUuids: [testFile.uuid] },
});
await user.click(editor.getByRole('button', { name: 'Close test.file' }));
expect(dispatch).toHaveBeenCalledWith(editorCloseFile(testFile.uuid));
});
it('should dispatch close action when delete button is pressed', async () => {
jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]);
jest.mocked(useFileStoragePath).mockReturnValue(testFile.path);
const [user, editor, dispatch] = testRender(, {
editor: { openFileUuids: [testFile.uuid] },
});
await user.type(editor.getByRole('tab', { name: 'test.file' }), '{Delete}');
expect(dispatch).toHaveBeenCalledWith(editorCloseFile(testFile.uuid));
});
it('should dispatch close action when tab is middle clicked', async () => {
jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]);
jest.mocked(useFileStoragePath).mockReturnValue(testFile.path);
const [user, editor, dispatch] = testRender(, {
editor: { openFileUuids: [testFile.uuid] },
});
await user.pointer({
keys: '[MouseMiddle]',
target: editor.getByRole('tab', { name: 'test.file' }),
});
expect(dispatch).toHaveBeenCalledWith(editorCloseFile(testFile.uuid));
});
});
describe('context menu', () => {
it('should hide the context menu when Escape is pressed', async () => {
const didCreate = new Promise((resolve) =>
monaco.editor.onDidCreateEditor(resolve),
);
const [user, editor] = testRender();
const code = await didCreate;
code.setModel(monaco.editor.createModel('test'));
expect(
editor.queryByRole('menu', { name: 'Editor context menu' }),
).toBeNull();
// HACK: monaco editor uses deprecated event fields (keyCode),
// so regular userEvent.type() doesn't work. testing library
// doesn't have ContextMenu in its keymap either.
fireEvent(
editor.getByRole('textbox', { name: /^Editor content/ }),
new KeyboardEvent('keydown', {
key: 'ContextMenu',
code: 'ContextMenu',
keyCode: 93,
}),
);
const contextMenu = await editor.findByRole('menu', {
name: 'Editor context menu',
});
expect(contextMenu).toBeInTheDocument();
// a11y: first item in menu should be focused when menu opens
await waitFor(() =>
expect(editor.getByRole('menuitem', { name: 'Copy' })).toHaveFocus(),
);
await user.keyboard('{Escape}');
await waitFor(() => expect(contextMenu).not.toBeInTheDocument());
// editor should be focused after context menu closes
expect(
editor.getByRole('textbox', { name: /^Editor content/ }),
).toHaveFocus();
});
it('should hide the context menu when clicking away', async () => {
const didCreate = new Promise((resolve) =>
monaco.editor.onDidCreateEditor(resolve),
);
const [user, editor] = testRender();
const code = await didCreate;
code.setModel(monaco.editor.createModel('test'));
expect(
editor.queryByRole('menu', { name: 'Editor context menu' }),
).toBeNull();
await user.pointer({
keys: '[MouseRight]',
target: editor.getByRole('textbox', { name: /^Editor content/ }),
});
const contextMenu = await editor.findByRole('menu', {
name: 'Editor context menu',
});
expect(contextMenu).toBeInTheDocument();
// a11y: first item in menu should be focused when menu opens
await waitFor(() =>
expect(editor.getByRole('menuitem', { name: 'Copy' })).toHaveFocus(),
);
const overlay = document
.getElementsByClassName(Classes.OVERLAY_BACKDROP)
.item(0);
defined(overlay);
await user.click(overlay);
await waitFor(() => expect(contextMenu).not.toBeInTheDocument());
// editor should be focused after context menu closes
expect(
editor.getByRole('textbox', { name: /^Editor content/ }),
).toHaveFocus();
});
});
});