diff --git a/Composer/cypress/integration/Breadcrumb.spec.js b/Composer/cypress/integration/Breadcrumb.spec.js
index 6ac7dd16e3..b6f94e0465 100644
--- a/Composer/cypress/integration/Breadcrumb.spec.js
+++ b/Composer/cypress/integration/Breadcrumb.spec.js
@@ -1,39 +1,72 @@
///
context('breadcrumb', () => {
- beforeEach(() => {
+ before(() => {
cy.visit(Cypress.env('COMPOSER_URL'));
cy.openBot('ToDoBot');
});
+ beforeEach(() => {
+ // Return to Main.dialog
+ cy.get('[data-testid="ProjectTree"]').within(() => {
+ cy.getByText('ToDoBot.Main').click();
+ });
+ });
+
it('can show dialog name in breadcrumb', () => {
+ // Should path = main dialog at first render
cy.getByTestId('Breadcrumb')
.invoke('text')
.should('contain', 'ToDoBot.Main');
+ // Click on AddToDo dialog
cy.get('[data-testid="ProjectTree"]').within(() => {
cy.getByText('AddToDo').click();
});
-
cy.getByTestId('Breadcrumb')
.invoke('text')
.should('contain', 'AddToDo');
+ // Return to Main.dialog
cy.get('[data-testid="ProjectTree"]').within(() => {
cy.getByText('ToDoBot.Main').click();
- cy.getByText('#AddIntent').click();
});
- cy.withinEditor('VisualEditor', () => {
- cy.getByText('AddIntent').click();
+ cy.getByTestId('Breadcrumb')
+ .invoke('text')
+ .should('contain', 'ToDoBot');
+ });
+
+ it('can show event name in breadcrumb', () => {
+ cy.get('[data-testid="ProjectTree"]').within(() => {
+ cy.getByText('AddToDo').click();
+ cy.wait(100);
+ cy.getByText('Handle an Event: BeginDialog').click();
cy.wait(100);
+ });
+
+ cy.getByTestId('Breadcrumb')
+ .invoke('text')
+ .should('match', /AddToDo.*Handle an Event.*/);
+ });
+
+ it('can show action name in breadcrumb', () => {
+ cy.wait(100);
+ cy.get('[data-testid="ProjectTree"]').within(() => {
+ cy.getByText('ToDoBot.Main').click();
+ cy.wait(500);
+ });
+
+ // Click on an action
+ cy.withinEditor('VisualEditor', () => {
cy.getByTestId('RuleEditor').within(() => {
- cy.getByText('AddToDo').click();
+ cy.getByText('Send an Activity').click();
+ cy.wait(500);
});
});
cy.getByTestId('Breadcrumb')
.invoke('text')
- .should('match', /ToDoBot.+AddToDo/);
+ .should('match', /ToDoBot.+Send an Activity/);
});
});
diff --git a/Composer/cypress/integration/CreateNewBot.spec.js b/Composer/cypress/integration/CreateNewBot.spec.js
index 24d47ef25e..98336733e4 100644
--- a/Composer/cypress/integration/CreateNewBot.spec.js
+++ b/Composer/cypress/integration/CreateNewBot.spec.js
@@ -3,13 +3,20 @@
context('Creating a new bot', () => {
beforeEach(() => {
cy.visit(Cypress.env('COMPOSER_URL'));
+ cy.wait(1000);
+ cy.get('[data-testid="LeftNav-CommandBarButtonHome"]').click();
+ cy.wait(5000);
+ cy.get('[data-testid="homePage-ToolBar-New"]').within(() => {
+ cy.getByText('New').click();
+ });
+ cy.wait(5000);
});
it('can create a new bot', () => {
- cy.get('[data-testid="LeftNav-CommandBarButtonHome"]').click();
- cy.get('[data-testid="homePage-ToolBar-New"]').click();
cy.get('input[data-testid="Create from scratch"]').click();
+ cy.wait(100);
cy.get('button[data-testid="NextStepButton"]').click();
+ cy.wait(100);
cy.get('input[data-testid="NewDialogName"]').type('__TestNewProject');
cy.get('input[data-testid="NewDialogName"]').type('{enter}');
cy.get('[data-testid="ProjectTree"]').within(() => {
@@ -18,10 +25,12 @@ context('Creating a new bot', () => {
});
it('can create a bot from the ToDo template', () => {
- cy.get('[data-testid="homePage-ToolBar-New"]').click();
cy.get('input[data-testid="Create from template"]').click({ force: true });
+ cy.wait(100);
cy.get('[data-testid="ToDoBot"]').click();
+ cy.wait(100);
cy.get('button[data-testid="NextStepButton"]').click();
+ cy.wait(100);
cy.get('input[data-testid="NewDialogName"]').type('__TestNewProject');
cy.get('input[data-testid="NewDialogName"]').type('{enter}');
cy.get('[data-testid="ProjectTree"]').within(() => {
diff --git a/Composer/cypress/integration/CursorMove.spec.js b/Composer/cypress/integration/CursorMove.spec.js
deleted file mode 100644
index 759176fc1a..0000000000
--- a/Composer/cypress/integration/CursorMove.spec.js
+++ /dev/null
@@ -1,73 +0,0 @@
-///
-require('cypress-plugin-tab');
-
-// this test is too unstable right now
-// re-enable when stablized
-context('Cursor move', () => {
- beforeEach(() => {
- cy.visit(Cypress.env('COMPOSER_URL'));
- cy.openBot('ToDoBot');
- });
- it('can move cursor by pressing direction key or tab', () => {
- cy.get('[data-testid="ProjectTree"]').within(() => {
- cy.getByText('AddToDo').click();
- cy.wait(100);
- });
-
- cy.withinEditor('VisualEditor', () => {
- cy.get('[data-is-node=true]').as('selectedNodes');
- cy.get('@selectedNodes')
- .eq(1)
- .click();
-
- // down arrow
- cy.get('[data-test-id="keyboard-zone"]').type('{downarrow}');
- cy.wait(100);
- cy.get('@selectedNodes')
- .eq(2)
- .should('have.class', 'step-renderer-container--selected');
-
- cy.wait(100);
-
- // right arrow
- cy.get('[data-test-id="keyboard-zone"]').type('{rightarrow}');
- cy.wait(100);
- cy.get('@selectedNodes')
- .eq(4)
- .should('have.class', 'step-renderer-container--selected');
-
- cy.wait(100);
-
- // left arrow
- cy.get('[data-test-id="keyboard-zone"]').type('{leftarrow}');
- cy.wait(100);
- cy.get('@selectedNodes')
- .eq(5)
- .should('have.class', 'step-renderer-container--selected');
-
- cy.wait(100);
-
- // up arrow
- cy.get('[data-test-id="keyboard-zone"]').type('{uparrow}');
- cy.wait(100);
- cy.get('@selectedNodes')
- .eq(3)
- .should('have.class', 'step-renderer-container--selected');
- cy.wait(100);
-
- cy.get('[data-is-selectable=true]').as('selectedElements');
-
- cy.get('@selectedElements')
- .eq(1)
- .click();
-
- // tab
- cy.get('[data-test-id="keyboard-zone"]').tab();
- cy.wait(100);
- cy.get('@selectedElements')
- .eq(2)
- .should('have.class', 'step-renderer-container--selected');
- cy.wait(100);
- });
- });
-});
diff --git a/Composer/cypress/integration/LUPage.spec.js b/Composer/cypress/integration/LUPage.spec.js
index 1d07fca875..e0ae983f8c 100644
--- a/Composer/cypress/integration/LUPage.spec.js
+++ b/Composer/cypress/integration/LUPage.spec.js
@@ -1,7 +1,7 @@
///
context('check language understanding page', () => {
- beforeEach(() => {
+ before(() => {
cy.visit(Cypress.env('COMPOSER_URL'));
cy.openBot('ToDoLuisBot');
});
diff --git a/Composer/cypress/integration/LuisDeploy.spec.js b/Composer/cypress/integration/LuisDeploy.spec.js
index dd32c89bab..8b2b546c16 100644
--- a/Composer/cypress/integration/LuisDeploy.spec.js
+++ b/Composer/cypress/integration/LuisDeploy.spec.js
@@ -31,6 +31,7 @@ context('Luis Deploy', () => {
response: 'fixture:luPublish/success',
});
cy.getByText('Start Bot').click();
+ cy.wait(5000);
// clear its settings before
cy.get('[data-testid="ProjectNameInput"]')
.clear()
@@ -44,6 +45,7 @@ context('Luis Deploy', () => {
// wait for the debounce interval of sync settings
cy.wait(1000);
cy.getByText('Publish').click();
+ cy.wait(1000);
cy.getByText('Restart Bot').should('exist');
cy.getByText('Test in Emulator').should('exist');
@@ -54,7 +56,9 @@ context('Luis Deploy', () => {
response: 'fixture:luPublish/error',
});
cy.getByText('Restart Bot').click();
+ cy.wait(1000);
cy.getByText('Try again').click();
+ cy.wait(1000);
cy.get('[data-testid="AuthoringKeyInput"]').type('no-id');
cy.getByText('Publish').click();
});
diff --git a/Composer/cypress/integration/RemoveDialog.spec.js b/Composer/cypress/integration/RemoveDialog.spec.js
index 31bd85e20b..1fc4c24dbe 100644
--- a/Composer/cypress/integration/RemoveDialog.spec.js
+++ b/Composer/cypress/integration/RemoveDialog.spec.js
@@ -16,7 +16,9 @@ context('RemoveDialog', () => {
.invoke('attr', 'style', 'visibility: visible')
.click();
});
- cy.getByText('Delete').click();
+ cy.get('.ms-ContextualMenu-linkContent > .ms-ContextualMenu-itemText').within(() => {
+ cy.getByText('Delete').click();
+ });
cy.getByText('Yes').click();
cy.get('[data-testid="ProjectTree"]').within(() => {
cy.get('[title="AddItem"]').should('not.exist');
diff --git a/Composer/cypress/integration/ToDoBot.spec.js b/Composer/cypress/integration/ToDoBot.spec.js
index c22df37fae..1ab7ca638b 100644
--- a/Composer/cypress/integration/ToDoBot.spec.js
+++ b/Composer/cypress/integration/ToDoBot.spec.js
@@ -1,7 +1,7 @@
///
context('ToDo Bot', () => {
- beforeEach(() => {
+ before(() => {
cy.visit(Cypress.env('COMPOSER_URL'));
cy.openBot('ToDoBot');
});
@@ -9,6 +9,7 @@ context('ToDo Bot', () => {
it('can open the main dialog', () => {
cy.get('[data-testid="ProjectTree"]').within(() => {
cy.getByText('ToDoBot.Main').click();
+ cy.wait(100);
});
cy.withinEditor('FormEditor', () => {
cy.getByDisplayValue('ToDoBot.Main').should('exist');
@@ -33,6 +34,7 @@ context('ToDo Bot', () => {
it('can open the ClearToDos dialog', () => {
cy.get('[data-testid="ProjectTree"]').within(() => {
cy.getByText('ClearToDos').click();
+ cy.wait(100);
});
cy.withinEditor('FormEditor', () => {
@@ -46,6 +48,7 @@ context('ToDo Bot', () => {
it('can open the DeleteToDo dialog', () => {
cy.get('[data-testid="ProjectTree"]').within(() => {
cy.getByText('DeleteToDo').click();
+ cy.wait(100);
});
cy.withinEditor('FormEditor', () => {
@@ -59,6 +62,7 @@ context('ToDo Bot', () => {
it('can open the ShowToDos dialog', () => {
cy.get('[data-testid="ProjectTree"]').within(() => {
cy.getByText('ShowToDos').click();
+ cy.wait(100);
});
cy.withinEditor('FormEditor', () => {
diff --git a/Composer/cypress/integration/VisualDesigner.spec.js b/Composer/cypress/integration/VisualDesigner.spec.js
index d77fafec8e..e0b5b8c8bc 100644
--- a/Composer/cypress/integration/VisualDesigner.spec.js
+++ b/Composer/cypress/integration/VisualDesigner.spec.js
@@ -1,39 +1,14 @@
///
context('Visual Designer', () => {
- beforeEach(() => {
+ before(() => {
cy.visit(Cypress.env('COMPOSER_URL'));
- cy.startFromTemplate('EmptyBot', 'VisualDesignerTest');
+ cy.openBot('ToDoBot');
});
- //will remove skip after add trigger is ok
- it('can add a rule from the visual designer', () => {
- cy.addEventHandler('Handle a Dialog Event');
- cy.wait(100);
-
- cy.withinEditor('VisualEditor', () => {
- cy.contains('Handle a Dialog Event').should('exist');
- });
-
- cy.addEventHandler('Handle an Intent');
- cy.wait(100);
-
- cy.withinEditor('VisualEditor', () => {
- cy.contains('Handle an Intent').should('exist');
- });
-
- cy.addEventHandler('Handle Unknown Intent');
- cy.wait(100);
-
- cy.withinEditor('VisualEditor', () => {
- cy.contains('Handle Unknown Intent').should('exist');
- });
-
- cy.addEventHandler('Handle ConversationUpdate');
- cy.wait(100);
-
+ it('can find Visual Designer default trigger in container', () => {
cy.withinEditor('VisualEditor', () => {
- cy.contains('Handle ConversationUpdate').should('exist');
+ cy.getByText('Trigger').should('exist');
});
});
});
diff --git a/Composer/cypress/support/commands.js b/Composer/cypress/support/commands.js
index 9d9fb50dd9..8550d6d2e2 100644
--- a/Composer/cypress/support/commands.js
+++ b/Composer/cypress/support/commands.js
@@ -26,7 +26,9 @@ import 'cypress-testing-library/add-commands';
Cypress.Commands.add('openBot', botName => {
cy.get('[data-testid="LeftNav-CommandBarButtonHome"]').click();
- cy.getByText('Open').click();
+ cy.get('[data-testid="homePage-ToolBar-Open"]').within(() => {
+ cy.getByText('Open').click();
+ });
cy.get('[data-testid="SelectLocation"]').within(() => {
cy.get(`[aria-label="${botName}"]`).click({ force: true });
cy.wait(500);
diff --git a/Composer/packages/client/src/ShellApi.ts b/Composer/packages/client/src/ShellApi.ts
index 7e5d7fd7bf..4f08ee333f 100644
--- a/Composer/packages/client/src/ShellApi.ts
+++ b/Composer/packages/client/src/ShellApi.ts
@@ -67,7 +67,7 @@ const shellNavigator = (shellPage: string, opts: { id?: string } = {}) => {
export const ShellApi: React.FC = () => {
const { state, actions } = useContext(StoreContext);
const { dialogs, schemas, lgFiles, luFiles, designPageLocation, focusPath, breadcrumb } = state;
- const updateDialog = useDebouncedFunc(actions.updateDialog);
+ const updateDialog = actions.updateDialog;
const updateLuFile = actions.updateLuFile; //if debounced, error can't pass to form
const updateLgFile = useDebouncedFunc(actions.updateLgFile);
const updateLgTemplate = useDebouncedFunc(actions.updateLgTemplate);
@@ -278,12 +278,6 @@ export const ShellApi: React.FC = () => {
}
}
- function flushUpdates() {
- if (updateDialog.flush) {
- updateDialog.flush();
- }
- }
-
function cleanData() {
const cleanedData = sanitizeDialogData(dialogsMap[dialogId]);
if (!isEqual(dialogsMap[dialogId], cleanedData)) {
@@ -293,7 +287,6 @@ export const ShellApi: React.FC = () => {
};
updateDialog(payload);
}
- flushUpdates();
}
function navTo({ path }) {
diff --git a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx
index 3824447a74..84306e5d61 100644
--- a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx
+++ b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx
@@ -114,6 +114,16 @@ function ExtensionContainer() {
}
});
+ apiClient.registerApi('rpc', (method, ...params) => {
+ const handler = (window as any)[method];
+ let result;
+ if (handler) {
+ result = handler(...params);
+ }
+
+ return result;
+ });
+
shellApi.getState().then(result => {
setShellData(result);
});
diff --git a/Composer/packages/client/src/messenger/FrameAPI.ts b/Composer/packages/client/src/messenger/FrameAPI.ts
new file mode 100644
index 0000000000..ce5a1303dc
--- /dev/null
+++ b/Composer/packages/client/src/messenger/FrameAPI.ts
@@ -0,0 +1,44 @@
+import ApiClient from './ApiClient';
+
+const apiClient = new ApiClient();
+
+export class FrameAPI {
+ frameId: string;
+ frameRef: any;
+
+ constructor(frameId: string) {
+ this.frameId = frameId;
+ this.frameRef = null;
+ }
+
+ /**
+ * Initialize the frame ref at first invocation.
+ */
+ invoke = (method: string, ...rest) => {
+ if (!this.frameRef) {
+ this.frameRef = window.frames[this.frameId];
+ }
+
+ if (this.frameRef && this.frameRef[method]) {
+ return apiClient.apiCall('rpc', [method, ...rest], this.frameRef);
+ }
+ return Promise.reject();
+ };
+}
+
+export const VisualEditorAPI = (() => {
+ const visualEditorFrameAPI = new FrameAPI('VisualEditor');
+ // HACK: under cypress env, avoid invoking API inside frame too frequently (especially the `hasEleemntFocused`). It will lead to CI test quite fagile.
+ // TODO: remove this hack logic after refactoring state sync logic between shell and editors.
+ if ((window as any).Cypress) {
+ visualEditorFrameAPI.invoke = () => Promise.resolve(false);
+ }
+
+ return {
+ hasElementFocused: () => visualEditorFrameAPI.invoke('hasElementFocused'),
+ hasElementSelected: () => visualEditorFrameAPI.invoke('hasElementSelected'),
+ copySelection: () => visualEditorFrameAPI.invoke('copySelection'),
+ cutSelection: () => visualEditorFrameAPI.invoke('cutSelection'),
+ deleteSelection: () => visualEditorFrameAPI.invoke('deleteSelection'),
+ };
+})();
diff --git a/Composer/packages/client/src/pages/design/index.js b/Composer/packages/client/src/pages/design/index.js
index b66fa5fbde..38cc1d02a3 100644
--- a/Composer/packages/client/src/pages/design/index.js
+++ b/Composer/packages/client/src/pages/design/index.js
@@ -4,6 +4,7 @@ import formatMessage from 'format-message';
import { globalHistory } from '@reach/router';
import { toLower, get } from 'lodash';
+import { VisualEditorAPI } from '../../messenger/FrameAPI';
import { TestController } from '../../TestController';
import { BASEPATH, DialogDeleting } from '../../constants';
import { getbreadcrumbLabel, deleteTrigger, createSelectedPath } from '../../utils';
@@ -88,6 +89,8 @@ function DesignPage(props) {
const { dialogId, selected } = designPageLocation;
const [triggerModalVisible, setTriggerModalVisibility] = useState(false);
const [triggerButtonVisible, setTriggerButtonVisibility] = useState(false);
+ const [nodeOperationAvailable, setNodeOperationAvailability] = useState(false);
+
useEffect(() => {
if (match) {
const { dialogId } = match;
@@ -155,6 +158,14 @@ function DesignPage(props) {
}
};
+ VisualEditorAPI.hasElementSelected()
+ .then(selected => {
+ setNodeOperationAvailability(selected);
+ })
+ .catch(() => {
+ setNodeOperationAvailability(false);
+ });
+
const toolbarItems = [
{
type: 'action',
@@ -180,6 +191,42 @@ function DesignPage(props) {
},
align: 'left',
},
+ {
+ type: 'action',
+ text: formatMessage('Cut'),
+ buttonProps: {
+ disabled: !nodeOperationAvailable,
+ iconProps: {
+ iconName: 'Cut',
+ },
+ onClick: () => VisualEditorAPI.cutSelection(),
+ },
+ align: 'left',
+ },
+ {
+ type: 'action',
+ text: formatMessage('Copy'),
+ buttonProps: {
+ disabled: !nodeOperationAvailable,
+ iconProps: {
+ iconName: 'Copy',
+ },
+ onClick: () => VisualEditorAPI.copySelection(),
+ },
+ align: 'left',
+ },
+ {
+ type: 'action',
+ text: formatMessage('Delete'),
+ buttonProps: {
+ disabled: !nodeOperationAvailable,
+ iconProps: {
+ iconName: 'Delete',
+ },
+ onClick: () => VisualEditorAPI.deleteSelection(),
+ },
+ align: 'left',
+ },
{
type: 'element',
element: ,
@@ -287,6 +334,7 @@ function DesignPage(props) {