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) {
{breadcrumbItems}