Skip to content
Open
1 change: 1 addition & 0 deletions src/documentSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const DocumentSource = {
playground: 'playground',
collectionview: 'collectionview',
codelens: 'codelens',
databrowser: 'databrowser',
} as const;

export type DocumentSource =
Expand Down
16 changes: 12 additions & 4 deletions src/editors/playgroundController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,12 @@ export default class PlaygroundController {
if (shouldConfirmRunCopilotCode === true) {
const name = this._connectionController.getActiveConnectionName();
const confirmRunCopilotCode = await vscode.window.showInformationMessage(
`Are you sure you want to run this code generated by the MongoDB participant against ${name}? This confirmation can be disabled in the extension settings.`,
{ modal: true },
`Are you sure you want to run this code generated by the MongoDB participant against ${name}?`,
{
modal: true,
detail:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As a drive-by I updated all these to put the extra text as detail so it is not just a narrow sea of text. I went looking for a way to make these modals wider because an object id typically does not fit in the width by 1 character. And this is all I found. Still makes these look a little bit better, though.

'This confirmation can be disabled in the extension settings.',
},
'Yes',
);

Expand Down Expand Up @@ -576,8 +580,12 @@ export default class PlaygroundController {
if (shouldConfirmRunAll === true) {
const name = this._connectionController.getActiveConnectionName();
const confirmRunAll = await vscode.window.showInformationMessage(
`Are you sure you want to run this playground against ${name}? This confirmation can be disabled in the extension settings.`,
{ modal: true },
`Are you sure you want to run this playground against ${name}?`,
{
modal: true,
detail:
'This confirmation can be disabled in the extension settings.',
},
'Yes',
);

Expand Down
6 changes: 4 additions & 2 deletions src/explorer/documentTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default class DocumentTreeItem

const documentLabel = document._id
? JSON.stringify(document._id)
: `Document ${documentIndexInTree + 1}`;
: `"Document ${documentIndexInTree + 1}"`;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As a drive-by I removed the double "" around the text that's usually JSON.stringified and therefore already has "" around it, but then in this case I had to add it back.


this.dataService = dataService;
this.document = document;
Expand Down Expand Up @@ -112,9 +112,11 @@ export default class DocumentTreeItem

if (shouldConfirmDeleteDocument === true) {
const confirmationResult = await vscode.window.showInformationMessage(
`Are you sure you wish to drop this document${this.tooltip ? ` "${this.tooltip}"` : ''}? This confirmation can be disabled in the extension settings.`,
`Are you sure you wish to drop this document${this.tooltip ? ` ${this.tooltip}` : ''}?`,
{
modal: true,
detail:
'This confirmation can be disabled in the extension settings.',
},
'Yes',
);
Expand Down
3 changes: 3 additions & 0 deletions src/mdbExtensionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ export default class MDBExtensionController implements vscode.Disposable {
});
this._dataBrowsingController = new DataBrowsingController({
connectionController: this._connectionController,
editorsController: this._editorsController,
playgroundController: this._playgroundController,
explorerController: this._explorerController,
telemetryService: this._telemetryService,
});
this._editorsController.registerProviders();
Expand Down
32 changes: 32 additions & 0 deletions src/test/suite/views/data-browsing-app/messageHandler.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
handleExtensionMessage,
setupMessageHandler,
} from '../../../../views/data-browsing-app/store/messageHandler';
import * as vscodeApi from '../../../../views/data-browsing-app/vscode-api';

describe('messageHandler test suite', function () {
afterEach(function () {
Expand Down Expand Up @@ -273,6 +274,37 @@ describe('messageHandler test suite', function () {
expect(store.getState().documentQuery.themeKind).to.equal('vs');
});
});

describe('documentDeleted', function () {
it('should request a refresh when documentDeleted message received', function () {
const store = createStore();

// Ensure we start with a non-loading state
handleExtensionMessage(store.dispatch, {
command: PreviewMessageType.loadPage,
documents: [{ _id: '1', name: 'Test' }],
});
expect(store.getState().documentQuery.isLoading).to.be.false;

// Stub the API calls that documentsRefreshRequested triggers
const sendGetDocumentsStub = sinon.stub(vscodeApi, 'sendGetDocuments');
const sendGetTotalCountStub = sinon.stub(
vscodeApi,
'sendGetTotalCount',
);

// Trigger documentDeleted message
handleExtensionMessage(store.dispatch, {
command: PreviewMessageType.documentDeleted,
});

// State should be set to loading and refresh API functions called
expect(store.getState().documentQuery.isLoading).to.be.true;
expect(store.getState().documentQuery.isLoading).to.be.true;
expect(sendGetDocumentsStub.calledOnce).to.be.true;
expect(sendGetTotalCountStub.calledOnce).to.be.true;
});
});
});

describe('setupMessageHandler', function () {
Expand Down
177 changes: 176 additions & 1 deletion src/test/suite/views/data-browsing-app/monaco-viewer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import React from 'react';
import { expect } from 'chai';
import sinon from 'sinon';
import { render, screen, cleanup, waitFor } from '@testing-library/react';
import {
render,
screen,
cleanup,
waitFor,
fireEvent,
} from '@testing-library/react';

import MonacoViewer from '../../../../views/data-browsing-app/monaco-viewer';
import * as vscodeApi from '../../../../views/data-browsing-app/vscode-api';

// Mock the Monaco Editor component
let mockEditorValue = '';
Expand Down Expand Up @@ -314,4 +321,172 @@ describe('MonacoViewer test suite', function () {
expect(container).to.exist;
});
});

describe('Action bar', function () {
let sendEditDocumentStub: sinon.SinonStub;
let sendCloneDocumentStub: sinon.SinonStub;
let sendDeleteDocumentStub: sinon.SinonStub;
let clipboardWriteTextStub: sinon.SinonStub;

beforeEach(function () {
sendEditDocumentStub = sinon.stub(vscodeApi, 'sendEditDocument');
sendCloneDocumentStub = sinon.stub(vscodeApi, 'sendCloneDocument');
sendDeleteDocumentStub = sinon.stub(vscodeApi, 'sendDeleteDocument');

if (!navigator.clipboard) {
(navigator as any).clipboard = {
writeText: async (): Promise<void> => {
return Promise.resolve();
},
};
} else if (!navigator.clipboard.writeText) {
(navigator.clipboard as any).writeText = async (): Promise<void> => {
return Promise.resolve();
};
}
clipboardWriteTextStub = sinon.stub(navigator.clipboard, 'writeText');
});

afterEach(function () {
sendEditDocumentStub.restore();
sendCloneDocumentStub.restore();
sendDeleteDocumentStub.restore();
clipboardWriteTextStub.restore();
});

it('should render all action buttons when document has _id', function () {
const document = { _id: '123', name: 'Test' };

render(<MonacoViewer document={document} themeKind="vs-dark" />);

const editButton = screen.queryByTitle('Edit');
const copyButton = screen.queryByTitle('Copy');
const cloneButton = screen.queryByTitle('Clone');
const deleteButton = screen.queryByTitle('Delete');

expect(editButton).to.exist;
expect(copyButton).to.exist;
expect(cloneButton).to.exist;
expect(deleteButton).to.exist;
});

it('should only render copy button when document has no _id', function () {
const document = { name: 'Test' };

render(<MonacoViewer document={document} themeKind="vs-dark" />);

const editButton = screen.queryByTitle('Edit');
const copyButton = screen.queryByTitle('Copy');
const cloneButton = screen.queryByTitle('Clone');
const deleteButton = screen.queryByTitle('Delete');

expect(editButton).to.not.exist;
expect(copyButton).to.exist;
expect(cloneButton).to.not.exist;
expect(deleteButton).to.not.exist;
});

it('should call sendEditDocument when edit button is clicked', function () {
const document = { _id: '123', name: 'Test' };

render(<MonacoViewer document={document} themeKind="vs-dark" />);

const editButton = screen.getByTitle('Edit');
fireEvent.click(editButton);

expect(sendEditDocumentStub.calledOnce).to.be.true;
expect(sendEditDocumentStub.calledWith('123')).to.be.true;
});

it('should call clipboard.writeText when copy button is clicked', async function () {
const document = { _id: '123', name: 'Test' };

render(<MonacoViewer document={document} themeKind="vs-dark" />);

const copyButton = screen.getByTitle('Copy');
fireEvent.click(copyButton);

await waitFor(() => {
expect(clipboardWriteTextStub.calledOnce).to.be.true;
const calledWith = clipboardWriteTextStub.firstCall.args[0];
expect(calledWith).to.be.a('string');
expect(calledWith).to.include('_id:');
expect(calledWith).to.include('123');
});
});

it('should call sendCloneDocument when clone button is clicked', function () {
const document = { _id: '123', name: 'Test' };

render(<MonacoViewer document={document} themeKind="vs-dark" />);

const cloneButton = screen.getByTitle('Clone');
fireEvent.click(cloneButton);

expect(sendCloneDocumentStub.calledOnce).to.be.true;
expect(sendCloneDocumentStub.calledWith(document)).to.be.true;
});

it('should call sendDeleteDocument when delete button is clicked', function () {
const document = { _id: '123', name: 'Test' };

render(<MonacoViewer document={document} themeKind="vs-dark" />);

const deleteButton = screen.getByTitle('Delete');
fireEvent.click(deleteButton);

expect(sendDeleteDocumentStub.calledOnce).to.be.true;
expect(sendDeleteDocumentStub.calledWith('123')).to.be.true;
});

it('should handle ObjectId _id correctly', function () {
const document = {
_id: { $oid: '507f1f77bcf86cd799439011' },
name: 'Test',
};

render(<MonacoViewer document={document} themeKind="vs-dark" />);

const editButton = screen.getByTitle('Edit');
fireEvent.click(editButton);

expect(sendEditDocumentStub.calledOnce).to.be.true;
expect(sendEditDocumentStub.firstCall.args[0]).to.deep.equal({
$oid: '507f1f77bcf86cd799439011',
});
});

it('should handle numeric _id correctly', function () {
const document = { _id: 42, name: 'Test' };

render(<MonacoViewer document={document} themeKind="vs-dark" />);

const deleteButton = screen.getByTitle('Delete');
fireEvent.click(deleteButton);

expect(sendDeleteDocumentStub.calledOnce).to.be.true;
expect(sendDeleteDocumentStub.calledWith(42)).to.be.true;
});

it('should have correct codicon icons for each button', function () {
const document = { _id: '123', name: 'Test' };

render(<MonacoViewer document={document} themeKind="vs-dark" />);

const editButton = screen.getByTitle('Edit');
const copyButton = screen.getByTitle('Copy');
const cloneButton = screen.getByTitle('Clone');
const deleteButton = screen.getByTitle('Delete');

const editIcon = editButton.querySelector('.codicon-edit');
const copyIcon = copyButton.querySelector('.codicon-copy');
const cloneIcon = cloneButton.querySelector('.codicon-files');
const deleteIcon = deleteButton.querySelector('.codicon-trash');

expect(editIcon).to.exist;
expect(copyIcon).to.exist;
expect(cloneIcon).to.exist;
expect(deleteIcon).to.exist;
});
});
});
Loading
Loading