diff --git a/.github/actions/conventional-pr/src/conventional-pr.ts b/.github/actions/conventional-pr/src/conventional-pr.ts index ddbd8fa028..6794cbc873 100644 --- a/.github/actions/conventional-pr/src/conventional-pr.ts +++ b/.github/actions/conventional-pr/src/conventional-pr.ts @@ -1,7 +1,13 @@ import * as core from '@actions/core'; import * as github from '@actions/github'; -import { validateTitle, validateBody } from './utils'; +import { + validateTitle, + validateBody, + validateBaseBranch, + PullRequestInfo, + isRelease, +} from './utils'; const OWNER = github.context.repo.owner; const REPO = github.context.repo.repo; @@ -22,6 +28,7 @@ const prQuery = ` pullRequest(number: $prNumber) { title body + baseRefName } } } @@ -43,22 +50,29 @@ async function run() { repo: REPO, prNumber, }); - const pr = repository.pullRequest; + const pr = repository?.pullRequest as PullRequestInfo; if (!pr) { core.setFailed('Not in a Pull Request context.'); return; } - const titleErrors = validateTitle(pr.title); - const bodyErrors = validateBody(pr.body); + if (!isRelease(pr)) { + const titleErrors = validateTitle(pr.title); + const bodyErrors = validateBody(pr.body); + const branchErrors = validateBaseBranch(pr.title, pr.baseRefName); - if (titleErrors.length) { - core.setFailed(titleErrors.join('\n')); - } + if (titleErrors.length) { + core.setFailed(titleErrors.join('\n')); + } - if (bodyErrors.length) { - core.setFailed(bodyErrors.join('\n')); + if (bodyErrors.length) { + core.setFailed(bodyErrors.join('\n')); + } + + if (branchErrors.length) { + core.setFailed(branchErrors.join('\n')); + } } } catch (err) { core.error(err); diff --git a/.github/actions/conventional-pr/src/utils.ts b/.github/actions/conventional-pr/src/utils.ts index fbcbc1f77b..407b03953f 100644 --- a/.github/actions/conventional-pr/src/utils.ts +++ b/.github/actions/conventional-pr/src/utils.ts @@ -1,5 +1,10 @@ import * as core from '@actions/core'; +export interface PullRequestInfo { + title: string; + body: string; + baseRefName: string; +} type ValidationResult = string[]; const validTypes = [ @@ -14,6 +19,7 @@ const validTypes = [ 'ci', 'chore', 'revert', + 'release', ]; const typeList = validTypes.map(t => ` - ${t}`).join('\n'); @@ -28,22 +34,46 @@ export function validateTitle(title: string): ValidationResult { const hastype = validTypes.some(t => title.startsWith(`${t}: `)); if (!hastype) { - core.info( - `[Title] Missing type in title. Choose from the following:\n${typeList}` + errors.push( + `[Title] Must start with type (ex. 'feat: ').\nThe valid types are:\n${typeList}` ); - errors.push("[Title] Must start with type. i.e. 'feat: '"); } return errors; } const refMatch = /(refs?|close(d|s)?|fix(ed|es)?) \#\d+/i; +const helpLink = + 'https://help.github.com/en/github/managing-your-work-on-github/closing-issues-using-keywords'; export function validateBody(body: string): ValidationResult { let errors: ValidationResult = []; if (!refMatch.test(body)) { - errors.push('[Body] Must reference an issue.'); + errors.push( + `[Body] Must reference an issue (ex. 'fixes #1234').\nSee ${helpLink} for more details.` + ); + } + + return errors; +} + +export function isRelease(pr: PullRequestInfo) { + return pr.title.startsWith('release: ') && pr.baseRefName === 'stable'; +} + +export function validateBaseBranch( + title: string, + baseBranch: string +): ValidationResult { + let errors: ValidationResult = []; + + if (title.startsWith('release: ') && baseBranch !== 'stable') { + errors.push("[Release] Release pull request must target 'stable' branch."); + } else if (baseBranch === 'stable') { + errors.push( + "[Branch] Pull requests cannot target 'stable' branch. Perhaps you meant to create a release or are targeting the wrong branch." + ); } return errors; diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c32f34e045..b3de1d17bf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,19 +42,12 @@ jobs: - name: yarn test:coverage run: yarn test:coverage working-directory: Composer - # secrets are not exposed to PRs opened by forks, so just skip this step if it is not defined - - name: Publish coverage results - run: | - if [[ -z $COVERALLS_REPO_TOKEN ]]; then - echo "Coveralls token not found. Skipping." - else - cat coverage/lcov.info | ./node_modules/.bin/coveralls - fi - working-directory: Composer - env: - COVERALLS_SERVICE_NAME: "Github Actions" - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - COVERALLS_GIT_BRANCH: ${{ github.ref }} + - name: Coveralls + uses: coverallsapp/github-action@master + continue-on-error: true + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: ./Composer/coverage/lcov.info botproject: name: BotProject @@ -74,3 +67,23 @@ jobs: - name: dotnet test run: dotnet test working-directory: BotProject/CSharp + + docker-build: + name: Docker Build + timeout-minutes: 20 + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: docker-compose build + run: docker-compose build + - name: Health check + run: | + docker-compose up -d + sleep 10 + curl -Is http://localhost:3000 | grep -q "200 OK" + shell: bash + - name: Cleanup + if: always() + run: docker-compose down diff --git a/.gitignore b/.gitignore index b891ea6262..2c0aab8a74 100644 --- a/.gitignore +++ b/.gitignore @@ -397,3 +397,7 @@ MyBots/* # VsCode Composer/.vscode/ + +# Docker App Data +.appdata +docker-compose.override.yml diff --git a/.vscode/launch.json b/.vscode/launch.json index 2506014c71..669e566005 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,14 +1,41 @@ { "version": "0.2.0", "configurations": [ + { + "type": "chrome", + "request": "attach", + "name": "Attach to Chrome", + "port": 9222, + "webRoot": "${workspaceFolder}" + }, + { + "name": "LSP Server", + "type": "node", + "request": "launch", + "args": [ + "${workspaceFolder}/Composer/packages/tools/language-servers/language-generation/demo/src/server.ts" + ], + "runtimeArgs": [ + "--nolazy", + "-r", + "${workspaceFolder}/Composer/node_modules/ts-node/register" + ], + "sourceMaps": true, + "cwd": "${workspaceFolder}/Composer/packages/tools/language-servers/language-generation/demo/src", + "protocol": "inspector", + }, { "type": "node", "request": "launch", "name": "Server: Launch", - "args": ["./build/server.js"], + "args": [ + "./build/server.js" + ], "preLaunchTask": "server: build", "restart": true, - "outFiles": ["./build/*"], + "outFiles": [ + "./build/*" + ], "envFile": "${workspaceFolder}/Composer/packages/server/.env", "outputCapture": "std", "cwd": "${workspaceFolder}/Composer/packages/server" @@ -20,8 +47,14 @@ "name": "Jest Debug", "program": "${workspaceRoot}/Composer/node_modules/jest/bin/jest", "stopOnEntry": false, - "args": ["--runInBand", "--env=jsdom", "--config=jest.config.js"], - "runtimeArgs": ["--inspect-brk"], + "args": [ + "--runInBand", + "--env=jsdom", + "--config=jest.config.js" + ], + "runtimeArgs": [ + "--inspect-brk" + ], "cwd": "${workspaceRoot}/Composer/packages/server", "sourceMaps": true, "console": "integratedTerminal" diff --git a/.vscode/settings.json b/.vscode/settings.json index 8b135ed2de..34244c1aef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,7 @@ { "language": "typescript", "autoFix": true }, { "language": "typescriptreact", "autoFix": true } ], + "eslint.workingDirectories": ["./Composer"], "editor.formatOnSave": true, "typescript.tsdk": "./Composer/node_modules/typescript/lib" -} \ No newline at end of file +} diff --git a/BotProject/CSharp/ComposerDialogs/Main/Main.dialog b/BotProject/CSharp/ComposerDialogs/Main/Main.dialog index d6cce90d1b..cb675db8c8 100644 --- a/BotProject/CSharp/ComposerDialogs/Main/Main.dialog +++ b/BotProject/CSharp/ComposerDialogs/Main/Main.dialog @@ -9,7 +9,7 @@ "actions": [ { "$type": "Microsoft.SendActivity", - "activity": "[bfdactivity-003038]" + "activity": "@{bfdactivity-003038()}" } ] } diff --git a/BotProject/CSharp/Controllers/BotController.cs b/BotProject/CSharp/Controllers/BotController.cs index 0b570a99f1..dc1ad5c782 100644 --- a/BotProject/CSharp/Controllers/BotController.cs +++ b/BotProject/CSharp/Controllers/BotController.cs @@ -27,6 +27,7 @@ public BotController(BotManager botManager) } [HttpPost] + [HttpGet] public async Task PostAsync() { // Delegate the processing of the HTTP POST to the adapter. diff --git a/BotProject/CSharp/Dockerfile b/BotProject/CSharp/Dockerfile index 8d0d33ba29..9893fa62bf 100644 --- a/BotProject/CSharp/Dockerfile +++ b/BotProject/CSharp/Dockerfile @@ -1,6 +1,6 @@ FROM mcr.microsoft.com/dotnet/core/sdk:2.1-alpine AS build -WORKDIR /app/botproject/csharp +WORKDIR /src/botproject/csharp COPY *.sln . COPY *.csproj . @@ -15,5 +15,6 @@ RUN dotnet publish -o out FROM mcr.microsoft.com/dotnet/core/aspnet:2.1-alpine AS runtime WORKDIR /app/botproject/csharp -COPY --from=build /app/botproject/csharp/out . -CMD ["dotnet", "BotProject.dll"] \ No newline at end of file +COPY --from=build /src/botproject/csharp/ComposerDialogs ./ComposerDialogs +COPY --from=build /src/botproject/csharp/out . +CMD ["dotnet", "BotProject.dll"] diff --git a/BotProject/CSharp/README.md b/BotProject/CSharp/README.md index 9326104c3e..7f9cbbefb2 100644 --- a/BotProject/CSharp/README.md +++ b/BotProject/CSharp/README.md @@ -1,5 +1,5 @@ ## Bot Project -Bot project is the launcher project for the bots written in declarative form (JSON), using the Composer, for the Bot Framework SDK. They follow pattern defined in [OBI](https://github.com/Microsoft/botframework-obi) format. +Bot project is the launcher project for the bots written in declarative form (JSON), using the Composer, for the Bot Framework SDK. ## Instructions for setting up the Bot Project runtime The Bot Project is a regular Bot Framework SDK V4 project. Before you can launch it from the emulator, you need to make sure you can run the bot. diff --git a/BotProject/CSharp/Startup.cs b/BotProject/CSharp/Startup.cs index aeb49fc25e..12358ce19d 100644 --- a/BotProject/CSharp/Startup.cs +++ b/BotProject/CSharp/Startup.cs @@ -54,6 +54,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) app.UseDefaultFiles(); app.UseStaticFiles(); + app.UseWebSockets(); //app.UseHttpsRedirection(); app.UseMvc(); diff --git a/BotProject/Templates/CSharp/Controllers/BotController.cs b/BotProject/Templates/CSharp/Controllers/BotController.cs index b1b38d5804..5085705abb 100644 --- a/BotProject/Templates/CSharp/Controllers/BotController.cs +++ b/BotProject/Templates/CSharp/Controllers/BotController.cs @@ -30,6 +30,7 @@ public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) } [HttpPost] + [HttpGet] public async Task PostAsync() { // Delegate the processing of the HTTP POST to the adapter. diff --git a/BotProject/Templates/CSharp/README.md b/BotProject/Templates/CSharp/README.md index 9326104c3e..7f9cbbefb2 100644 --- a/BotProject/Templates/CSharp/README.md +++ b/BotProject/Templates/CSharp/README.md @@ -1,5 +1,5 @@ ## Bot Project -Bot project is the launcher project for the bots written in declarative form (JSON), using the Composer, for the Bot Framework SDK. They follow pattern defined in [OBI](https://github.com/Microsoft/botframework-obi) format. +Bot project is the launcher project for the bots written in declarative form (JSON), using the Composer, for the Bot Framework SDK. ## Instructions for setting up the Bot Project runtime The Bot Project is a regular Bot Framework SDK V4 project. Before you can launch it from the emulator, you need to make sure you can run the bot. diff --git a/BotProject/Templates/CSharp/Startup.cs b/BotProject/Templates/CSharp/Startup.cs index 172c9aa6f5..5b0d85f24f 100644 --- a/BotProject/Templates/CSharp/Startup.cs +++ b/BotProject/Templates/CSharp/Startup.cs @@ -131,6 +131,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) app.UseDefaultFiles(); app.UseStaticFiles(); + app.UseWebSockets(); //app.UseHttpsRedirection(); app.UseMvc(); diff --git a/CHANGELOG.md b/CHANGELOG.md index 3840c6b5ad..9b58fb16ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,18 +2,79 @@ ## Releases +### 12-10-2019 + +#### Added + +- feat: show error message in form editor. ([#1737](https://github.com/microsoft/BotFramework-Composer/pull/1737)) ([@alanlong9278](https://github.com/alanlong9278)) +- feat: link to tab ([#1738](https://github.com/microsoft/BotFramework-Composer/pull/1738)) ([@lei9444](https://github.com/lei9444)) +- feat: Deep linking for the notification page ([#1667](https://github.com/microsoft/BotFramework-Composer/pull/1667)) ([@lei9444](https://github.com/lei9444)) +- feat: Align with the new design in form for inline error display ([#1683](https://github.com/microsoft/BotFramework-Composer/pull/1683)) ([@alanlong9278](https://github.com/alanlong9278)) +- feat: LG LSP in Composer ([#1504](https://github.com/microsoft/BotFramework-Composer/pull/1504)) ([@zhixzhan](https://github.com/zhixzhan)) +- feat: Trigger Node ([#1529](https://github.com/microsoft/BotFramework-Composer/pull/1529)) ([@yeze322](https://github.com/yeze322)) +- feat: support default path environment variable ([#1652](https://github.com/microsoft/BotFramework-Composer/pull/1652)) ([@liweitian](https://github.com/liweitian)) +- feat: add directlinespeech support ([#1637](https://github.com/microsoft/BotFramework-Composer/pull/1637)) ([@xieofxie](https://github.com/xieofxie)) + +#### Fixed + +- fix: minor styling and labeling for linting ux ([#1716](https://github.com/microsoft/BotFramework-Composer/pull/1716)) ([@cwhitten](https://github.com/cwhitten)) +- fix: visual editor lg template don't show ([#1707](https://github.com/microsoft/BotFramework-Composer/pull/1707)) ([@zhixzhan](https://github.com/zhixzhan)) +- fix: location select Content component ([#1668](https://github.com/microsoft/BotFramework-Composer/pull/1668)) ([@liweitian](https://github.com/liweitian)) +- fix: ability to view storages when in local dev on mac ([#1696](https://github.com/microsoft/BotFramework-Composer/pull/1696)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- fix: dialog name incorrect when creating new dialog in form editor ([#1605](https://github.com/microsoft/BotFramework-Composer/pull/1605)) ([@alanlong9278](https://github.com/alanlong9278)) +- fix: support horizontal scrolling in visual eidtor ([#1607](https://github.com/microsoft/BotFramework-Composer/pull/1607)) ([@alanlong9278](https://github.com/alanlong9278)) +- fix: Fix interruption sample ([#1624](https://github.com/microsoft/BotFramework-Composer/pull/1624)) ([@luhan2017](https://github.com/luhan2017)) +- fix: fix minor LG ref syntax in CardSample ([#1749](https://github.com/microsoft/BotFramework-Composer/pull/1749)) ([@boydc2014](https://github.com/boydc2014)) +- fix: add fault tolerance for syntax highlighting ([#1690](https://github.com/microsoft/BotFramework-Composer/pull/1690)) ([@cosmicshuai](https://github.com/cosmicshuai)) +- fix: lu change doesn't reflect on form editor ([#1704](https://github.com/microsoft/BotFramework-Composer/pull/1704)) ([@zhixzhan](https://github.com/zhixzhan)) +- fix: one lg template error mess up others ([#1733](https://github.com/microsoft/BotFramework-Composer/pull/1733)) ([@zhixzhan](https://github.com/zhixzhan)) + +#### Changed + +- style: Updated Array UI ([#1617](https://github.com/microsoft/BotFramework-Composer/pull/1617)) ([@tdurnford](https://github.com/tdurnford)) +- style: update visual editor action title style ([#1710](https://github.com/microsoft/BotFramework-Composer/pull/1710)) ([@yeze322](https://github.com/yeze322)) +- refactor: upgrade lg parser and syntax ([#1676](https://github.com/microsoft/BotFramework-Composer/pull/1676)) ([@zhixzhan](https://github.com/zhixzhan)) +- refactor: centralize lg parsing logic to 'shared' lib ([#1663](https://github.com/microsoft/BotFramework-Composer/pull/1663)) ([@yeze322](https://github.com/yeze322)) +- refactor: convert cypress tests to typescript ([#1630](https://github.com/microsoft/BotFramework-Composer/pull/1630)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- style: update LGTheme ([#1706](https://github.com/microsoft/BotFramework-Composer/pull/1706)) ([@cosmicshuai](https://github.com/cosmicshuai)) + +#### Other + +- docs: R7 Doc Release ([#1743](https://github.com/microsoft/BotFramework-Composer/pull/1743)) ([@Kaiqb](https://github.com/Kaiqb)) +- docs: update coveralls badge ([#1621](https://github.com/microsoft/BotFramework-Composer/pull/1621)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- ci: disallow opening pr against stable branch unless a release ([#1740](https://github.com/microsoft/BotFramework-Composer/pull/1740)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- build: add ability to configure runtime path ([#1713](https://github.com/microsoft/BotFramework-Composer/pull/1713)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- build: make docker great again ([#1709](https://github.com/microsoft/BotFramework-Composer/pull/1709)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- chore: Adds license fields, fixes incorrect link, hides some left-nav elements ([#1691](https://github.com/microsoft/BotFramework-Composer/pull/1691)) ([@cwhitten](https://github.com/cwhitten)) +- chore: update typescript, eslint and prettier ([#1686](https://github.com/microsoft/BotFramework-Composer/pull/1686)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- build: give more memory available to Node in docker ([#1670](https://github.com/microsoft/BotFramework-Composer/pull/1670)) ([@benbrown](https://github.com/benbrown)) +- chore: add startup script to check for oudated versions ([#1674](https://github.com/microsoft/BotFramework-Composer/pull/1674)) ([@cwhitten](https://github.com/cwhitten)) +- ci: correctly clean up server process after e2e tests ([#1666](https://github.com/microsoft/BotFramework-Composer/pull/1666)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- chore: enforce node >=12 ([#1665](https://github.com/microsoft/BotFramework-Composer/pull/1665)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- ci: run cypress in single job for now ([#1658](https://github.com/microsoft/BotFramework-Composer/pull/1658)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- ci: do not fail CI if coveralls step fails ([#1655](https://github.com/microsoft/BotFramework-Composer/pull/1655)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- chore: reduce form width to 400px ([#1648](https://github.com/microsoft/BotFramework-Composer/pull/1648)) ([@cwhitten](https://github.com/cwhitten)) +- chore: bump browserslist ([#1645](https://github.com/microsoft/BotFramework-Composer/pull/1645)) ([@cwhitten](https://github.com/cwhitten)) +- test: allow running composer in hosted mode for tests ([#1356](https://github.com/microsoft/BotFramework-Composer/pull/1356)) ([@p-nagpal](https://github.com/p-nagpal)) +- ci: include better information in validate-pr action errors ([#1634](https://github.com/microsoft/BotFramework-Composer/pull/1634)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- chore: add browserslist to dependencies ([#1656](https://github.com/microsoft/BotFramework-Composer/pull/1656)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- build: make update script cross platform compatible ([#1687](https://github.com/microsoft/BotFramework-Composer/pull/1687)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) + ### 11-20-2019 #### Added -* linting and validation UI (#1518) (@lei9444) + +- linting and validation UI (#1518) (@lei9444) #### Changed -* improve build speed and bundle size (#1555) (@a-b-r-o-w-n) -* update `Conversation Started` trigger to `Greeting (Conversation Update)` (#1584) (@liweitian) + +- improve build speed and bundle size (#1555) (@a-b-r-o-w-n) +- update `Conversation Started` trigger to `Greeting (Conversation Update)` (#1584) (@liweitian) #### Fixed -* write QnA Maker endpointKey to settings (#1571) (@VanyLaw) -* fix docs typos (#1575) (@v-kydela) -* prevent double render in visual editor (#1601) (@yeze322) -* fix issue installing lubuild (#1606) (@lei9444) -* fix docker build (#1615) (@cwhitten) \ No newline at end of file + +- write QnA Maker endpointKey to settings (#1571) (@VanyLaw) +- fix docs typos (#1575) (@v-kydela) +- prevent double render in visual editor (#1601) (@yeze322) +- fix issue installing lubuild (#1606) (@lei9444) +- fix docker build (#1615) (@cwhitten) diff --git a/Composer/.dockerignore b/Composer/.dockerignore index c3066a3e4b..19245eab5f 100644 --- a/Composer/.dockerignore +++ b/Composer/.dockerignore @@ -7,4 +7,7 @@ **/server/tmp.zip # not ignore all lib folder because packages/lib, so probably we should rename that to libs packages/lib/*/lib -packages/extensions/*/lib \ No newline at end of file +packages/extensions/*/lib + +Dockerfile +.dockerignore diff --git a/Composer/.eslintrc.js b/Composer/.eslintrc.js index 678fddde71..39a1885f6e 100644 --- a/Composer/.eslintrc.js +++ b/Composer/.eslintrc.js @@ -3,7 +3,6 @@ module.exports = { 'eslint:recommended', 'plugin:prettier/recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:@typescript-eslint/eslint-recommended', 'prettier/@typescript-eslint', 'plugin:@bfc/bfcomposer/recommended', @@ -27,6 +26,7 @@ module.exports = { '@typescript-eslint/ban-ts-ignore': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-unnecessary-type-assertion': 'off', '@typescript-eslint/no-use-before-define': 'warn', diff --git a/Composer/.gitignore b/Composer/.gitignore index 51892b75d0..1603d74bba 100644 --- a/Composer/.gitignore +++ b/Composer/.gitignore @@ -4,3 +4,5 @@ junit.xml cypress/screenshots cypress/results cypress/videos + +TestBots/ diff --git a/Composer/Dockerfile b/Composer/Dockerfile index e78104a606..351572a63c 100644 --- a/Composer/Dockerfile +++ b/Composer/Dockerfile @@ -1,38 +1,34 @@ -FROM node:12-alpine as build -WORKDIR /src/Composer - -# yes, docker copy is really this stupid, https://github.com/moby/moby/issues/15858 -COPY yarn.lock . -COPY .npmrc . -COPY package.json . -COPY packages/client/package.json ./packages/client/ -COPY yarn.lock ./packages/server/ -COPY packages/server/package.json ./packages/server/ -COPY packages/lib/package.json ./packages/lib/ -COPY packages/lib/code-editor/package.json ./packages/lib/code-editor/ -COPY packages/lib/shared/package.json ./packages/lib/shared/ -COPY packages/lib/indexers/package.json ./packages/lib/indexers/ -COPY packages/extensions/package.json ./packages/extensions/ -COPY packages/lib/eslint-plugin-bfcomposer/package.json ./packages/lib/eslint-plugin-bfcomposer/ -COPY packages/extensions/obiformeditor/package.json ./packages/extensions/obiformeditor/ -COPY packages/extensions/visual-designer/package.json ./packages/extensions/visual-designer/ +################# +# +# Because Composer is organized as a monorepo with multiple packages +# managed by yarn workspaces, our Dockerfile may not look like other +# node / react projects. Specifically, we have to add all source files +# before doing yarn install due to yarn workspace symlinking. +# +################ -# run yarn install as a distinct layer -RUN yarn install +FROM node:12-alpine as build +WORKDIR /src/Composer COPY . . +# run yarn install as a distinct layer +RUN yarn install --frozen-lock-file +ENV NODE_OPTIONS "--max-old-space-size=4096" +ENV NODE_ENV "production" RUN yarn build:prod + # use a multi-stage build to reduce the final image size FROM node:12-alpine -WORKDIR /app/Composer/server -COPY --from=build /src/Composer/.npmrc . -COPY --from=build /src/Composer/packages/lib ./lib -COPY --from=build /src/Composer/packages/server . - -# update server package json to point to local packages -RUN node ./prepare-prod.js +WORKDIR /app/Composer +COPY --from=build /src/Composer/yarn.lock . +COPY --from=build /src/Composer/package.json . +COPY --from=build /src/Composer/packages/server ./packages/server +COPY --from=build /src/Composer/packages/lib ./packages/lib +COPY --from=build /src/Composer/packages/tools ./packages/tools -RUN yarn --production && yarn cache clean -CMD ["node", "build/server.js"] +ENV NODE_ENV "production" +RUN yarn --production --frozen-lockfile --force && yarn cache clean +WORKDIR /app/Composer +CMD ["yarn", "start:server"] diff --git a/Composer/README.md b/Composer/README.md index 50a7c25158..c4395a8faa 100644 --- a/Composer/README.md +++ b/Composer/README.md @@ -1,5 +1,5 @@ -[![Build Status](https://github.com/microsoft/BotFramework-Composer/workflows/Composer%20CI/badge.svg?branch=stable)](https://github.com/microsoft/BotFramework-Composer/actions?query=branch%3Astable) -[![Coverage Status](https://coveralls.io/repos/github/microsoft/BotFramework-Composer/badge.svg?branch=stable)](https://coveralls.io/github/microsoft/BotFramework-Composer?branch=stable) +[![Build Status](https://github.com/microsoft/BotFramework-Composer/workflows/Composer%20CI/badge.svg?branch=master)](https://github.com/microsoft/BotFramework-Composer/actions?query=branch%3Amaster) +[![Coverage Status](https://coveralls.io/repos/github/microsoft/BotFramework-Composer/badge.svg?branch=master)](https://coveralls.io/github/microsoft/BotFramework-Composer?branch=master) [![Total alerts](https://img.shields.io/lgtm/alerts/g/microsoft/BotFramework-Composer.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/microsoft/BotFramework-Composer/alerts/) # Composer @@ -8,7 +8,7 @@ The web app that can edit bots in OBI format, and can use Bot Launcher to run bo ### Instructions Prerequisite: -* node > 10.0 +* node > 12 * yarn // npm install -g yarn To build for hosting as site extension diff --git a/Composer/cypress.json b/Composer/cypress.json index 2146eadef4..a1ba87574c 100644 --- a/Composer/cypress.json +++ b/Composer/cypress.json @@ -4,6 +4,7 @@ "**/examples/*", "*.hot-update.js" ], + "supportFile": "cypress/support/index.ts", "video": false, "videoUploadOnPasses": false, "viewportWidth": 1600, diff --git a/Composer/cypress/.eslintrc.js b/Composer/cypress/.eslintrc.js new file mode 100644 index 0000000000..89d2a80a8c --- /dev/null +++ b/Composer/cypress/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['../.eslintrc.js', 'plugin:cypress/recommended'], +}; diff --git a/Composer/cypress/integration/Breadcrumb.spec.js b/Composer/cypress/integration/Breadcrumb.spec.js deleted file mode 100644 index 3a00e21e28..0000000000 --- a/Composer/cypress/integration/Breadcrumb.spec.js +++ /dev/null @@ -1,75 +0,0 @@ -/// - -context('breadcrumb', () => { - - beforeEach(() => { - cy.visit(Cypress.env('COMPOSER_URL')); - cy.createBot('TodoSample'); - cy.wait(100); - - // Return to Main.dialog - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.wait(1000); - cy.getByText('__TestTodoSample.Main').click(); - cy.wait(1000); - }); - }); - - it('can show dialog name in breadcrumb', () => { - // Should path = main dialog at first render - cy.getByTestId('Breadcrumb') - .invoke('text') - .should('contain', '__TestTodoSample.Main'); - - // Click on AddToDo dialog - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('AddToDo').click(); - }); - cy.getByTestId('Breadcrumb') - .invoke('text') - .should('contain', 'AddToDo'); - cy.wait(1000); - // Return to Main.dialog - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('__TestTodoSample.Main').click(); - cy.wait(100); - }); - - cy.getByTestId('Breadcrumb') - .invoke('text') - .should('contain', '__TestTodoSample'); - }); - - it('can show event name in breadcrumb', () => { - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('AddToDo').click(); - cy.wait(100); - cy.getByText('Dialog started (BeginDialog)').click(); - cy.wait(100); - }); - - cy.getByTestId('Breadcrumb') - .invoke('text') - .should('match', /AddToDo.*Dialog started (BeginDialog)*/); - }); - - it('can show action name in breadcrumb', () => { - cy.wait(100); - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('Greeting (ConversationUpdate)').click(); - cy.wait(500); - }); - - // Click on an action - cy.withinEditor('VisualEditor', () => { - cy.getByTestId('RuleEditor').within(() => { - cy.getByText('Send a response').click(); - cy.wait(500); - }); - }); - - cy.getByTestId('Breadcrumb') - .invoke('text') - .should('match', /__TestTodoSample.Main.*Greeting \(ConversationUpdate\).*Send a response/); - }); -}); diff --git a/Composer/cypress/integration/Breadcrumb.spec.ts b/Composer/cypress/integration/Breadcrumb.spec.ts new file mode 100644 index 0000000000..63adecd879 --- /dev/null +++ b/Composer/cypress/integration/Breadcrumb.spec.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('breadcrumb', () => { + beforeEach(() => { + cy.visit(Cypress.env('COMPOSER_URL')); + cy.createBot('TodoSample'); + + // Return to Main.dialog + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('__TestTodoSample.Main').click(); + }); + }); + + function hasBreadcrumbItems(cy: Cypress.cy, items: (string | RegExp)[]) { + cy.get('[data-testid="Breadcrumb"]') + .last() + .get('li') + .should($li => { + items.forEach((item, idx) => { + expect($li.eq(idx)).to.contain(item); + }); + }); + } + + it('can show dialog name in breadcrumb', () => { + // Should path = main dialog at first render + hasBreadcrumbItems(cy, ['__TestTodoSample.Main']); + + // Click on AddToDo dialog + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('AddToDo').click(); + }); + hasBreadcrumbItems(cy, ['AddToDo']); + + // Return to Main.dialog + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('__TestTodoSample.Main').click(); + }); + + hasBreadcrumbItems(cy, ['__TestTodoSample.Main']); + }); + + it('can show event name in breadcrumb', () => { + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('AddToDo').click(); + cy.findByText('Dialog started').click(); + }); + + hasBreadcrumbItems(cy, ['AddToDo', 'Dialog started']); + }); + + it('can show action name in breadcrumb', () => { + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('Greeting').click(); + }); + + // Click on an action + cy.withinEditor('VisualEditor', () => { + cy.findByTestId('RuleEditor').within(() => { + cy.findByText('Send a response').click(); + }); + }); + + hasBreadcrumbItems(cy, ['__TestTodoSample.Main', 'Greeting', 'Send a response']); + }); +}); diff --git a/Composer/cypress/integration/CreateNewBot.spec.js b/Composer/cypress/integration/CreateNewBot.spec.js deleted file mode 100644 index dff154be2a..0000000000 --- a/Composer/cypress/integration/CreateNewBot.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -/// - -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('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(() => { - cy.getByText('__TestNewProject.Main').should('exist'); - }); - }); - - it('can create a bot from the ToDo template', () => { - cy.get('input[data-testid="Create from template"]').click({ force: true }); - cy.wait(100); - cy.get('[data-testid="TodoSample"]').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(() => { - cy.getByText('__TestNewProject.Main').should('exist'); - cy.getByText('AddToDo').should('exist'); - cy.getByText('ClearToDos').should('exist'); - cy.getByText('DeleteToDo').should('exist'); - cy.getByText('ShowToDos').should('exist'); - }); - }); -}); diff --git a/Composer/cypress/integration/CreateNewBot.spec.ts b/Composer/cypress/integration/CreateNewBot.spec.ts new file mode 100644 index 0000000000..4841f3cde7 --- /dev/null +++ b/Composer/cypress/integration/CreateNewBot.spec.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('Creating a new bot', () => { + beforeEach(() => { + cy.visit(Cypress.env('COMPOSER_URL')); + cy.findByTestId('LeftNav-CommandBarButtonHome').click(); + cy.findByTestId('homePage-ToolBar-New').within(() => { + cy.findByText('New').click(); + }); + }); + + it('can create a new bot', () => { + cy.findByTestId('Create from scratch').click(); + cy.findByTestId('NextStepButton').click(); + cy.findByTestId('NewDialogName').type('{selectall}__TestNewProject{enter}'); + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('__TestNewProject.Main').should('exist'); + }); + }); + + it('can create a bot from the ToDo template', () => { + cy.findByTestId('Create from template').click(); + cy.findByTestId('TodoSample').click(); + cy.findByTestId('NextStepButton').click(); + cy.findByTestId('NewDialogName').type('{selectall}__TestNewProject2{enter}'); + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('__TestNewProject2.Main').should('exist'); + cy.findByText('AddToDo').should('exist'); + cy.findByText('ClearToDos').should('exist'); + cy.findByText('DeleteToDo').should('exist'); + cy.findByText('ShowToDos').should('exist'); + }); + }); +}); diff --git a/Composer/cypress/integration/HomePage.spec.ts b/Composer/cypress/integration/HomePage.spec.ts new file mode 100644 index 0000000000..cdd82aa5aa --- /dev/null +++ b/Composer/cypress/integration/HomePage.spec.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('Home Page ', () => { + beforeEach(() => { + cy.visit(Cypress.env('COMPOSER_URL')); + }); + + it('can open buttons in home page', () => { + cy.findByTestId('LeftNav-CommandBarButtonHome').click(); + cy.findByTestId('homePage-ToolBar-New').click(); + cy.findByText('Create from scratch?').should('exist'); + cy.findByText('Cancel').should('exist'); + cy.findByText('Cancel').click(); + cy.findByTestId('homePage-ToolBar-Open').click(); + cy.findByText('Select a Bot').should('exist'); + cy.findByText('Cancel').should('exist'); + cy.findByText('Cancel').click(); + cy.findByTestId('homePage-body-New').click(); + cy.findByText('Create from scratch?').should('exist'); + }); +}); diff --git a/Composer/cypress/integration/LGPage.spec.js b/Composer/cypress/integration/LGPage.spec.ts similarity index 64% rename from Composer/cypress/integration/LGPage.spec.js rename to Composer/cypress/integration/LGPage.spec.ts index cf9d281e7b..a10f2c7c82 100644 --- a/Composer/cypress/integration/LGPage.spec.js +++ b/Composer/cypress/integration/LGPage.spec.ts @@ -1,13 +1,14 @@ -/// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. -context('check language generation page', () => { +context('LG Page', () => { beforeEach(() => { cy.visit(Cypress.env('COMPOSER_URL')); cy.createBot('TodoSample'); }); it('can open language generation page', () => { - cy.get('[data-testid="LeftNav-CommandBarButtonBot Responses"]').click(); + cy.findByTestId('LeftNav-CommandBarButtonBot Responses').click(); // left nav tree cy.contains('TodoSample.Main'); cy.contains('All'); @@ -15,17 +16,21 @@ context('check language generation page', () => { cy.get('.toggleEditMode button').as('switchButton'); // by default is table view - cy.get('[data-testid="LGEditor"] [data-testid="table-view"]').should('exist'); + cy.findByTestId('LGEditor') + .findByTestId('table-view') + .should('exist'); // goto edit-mode cy.get('@switchButton').click(); - cy.get('[data-testid="LGEditor"] .monaco-editor').should('exist'); + cy.findByTestId('LGEditor') + .get('.monaco-editor') + .should('exist'); // back to table view cy.get('@switchButton').click(); // nav to Main dialog cy.get('.dialogNavTree a[title="__TestTodoSample.Main"]').click(); - cy.wait(300); + // cy.wait(300); // dialog filter, edit mode button is disabled. cy.get('@switchButton').should('be.disabled'); diff --git a/Composer/cypress/integration/LUPage.spec.js b/Composer/cypress/integration/LUPage.spec.ts similarity index 63% rename from Composer/cypress/integration/LUPage.spec.js rename to Composer/cypress/integration/LUPage.spec.ts index c10e15bc76..4f762924b1 100644 --- a/Composer/cypress/integration/LUPage.spec.js +++ b/Composer/cypress/integration/LUPage.spec.ts @@ -1,13 +1,14 @@ -/// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. -context('check language understanding page', () => { +context('LU Page', () => { before(() => { cy.visit(Cypress.env('COMPOSER_URL')); cy.createBot('ToDoBotWithLuisSample'); }); it('can open language understanding page', () => { - cy.get('[data-testid="LeftNav-CommandBarButtonUser Input"]').click(); + cy.findByTestId('LeftNav-CommandBarButtonUser Input').click(); // left nav tree cy.contains('ToDoBotWithLuisSample.Main'); @@ -19,18 +20,23 @@ context('check language understanding page', () => { cy.get('@switchButton').should('be.disabled'); // by default is table view - cy.get('[data-testid="LUEditor"] [data-testid="table-view"]').should('exist'); + cy.findByTestId('LUEditor') + .findByTestId('table-view') + .should('exist'); // nav to ToDoBotWithLuisSample.main dialog cy.get('.dialogNavTree button[title="__TestToDoBotWithLuisSample.Main"]').click({ multiple: true }); - cy.wait(300); // goto edit-mode cy.get('@switchButton').click(); - cy.get('[data-testid="LUEditor"] .monaco-editor').should('exist'); + cy.findByTestId('LUEditor') + .get('.monaco-editor') + .should('exist'); // back to all table view cy.get('.dialogNavTree button[title="All"]').click(); - cy.get('[data-testid="LUEditor"] [data-testid="table-view"]').should('exist'); + cy.findByTestId('LUEditor') + .findByTestId('table-view') + .should('exist'); }); }); diff --git a/Composer/cypress/integration/LeftNavBar.spec.js b/Composer/cypress/integration/LeftNavBar.spec.js deleted file mode 100644 index 130c66ead2..0000000000 --- a/Composer/cypress/integration/LeftNavBar.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -/// -context('check Nav Expandion ', () => { - beforeEach(() => { - cy.visit(Cypress.env('COMPOSER_URL')); - cy.createBot('TodoSample'); - }); - - it('can expand left Nav Bar', () => { - cy.get('[data-testid="LeftNavButton"]').click(); - cy.get('[data-testid="LeftNav-CommandBarButtonDesign Flow"]').should('exist'); - cy.get('[data-testid="LeftNav-CommandBarButtonBot Responses"]').click(); - cy.url().should('include', 'language-generation'); - cy.get('[data-testid="LeftNav-CommandBarButtonUser Input"]').click(); - cy.url().should('include', 'language-understanding'); - cy.get('[data-testid="LeftNav-CommandBarButtonSettings"]').click(); - cy.url().should('include', 'setting'); - }); -}); diff --git a/Composer/cypress/integration/LeftNavBar.spec.ts b/Composer/cypress/integration/LeftNavBar.spec.ts new file mode 100644 index 0000000000..780189025d --- /dev/null +++ b/Composer/cypress/integration/LeftNavBar.spec.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('Left Nav Bar', () => { + beforeEach(() => { + cy.visit(Cypress.env('COMPOSER_URL')); + cy.createBot('TodoSample'); + }); + + it('can expand left Nav Bar', () => { + cy.findByTestId('LeftNavButton').click(); + cy.findByTestId('LeftNav-CommandBarButtonDesign Flow').should('exist'); + cy.findByTestId('LeftNav-CommandBarButtonBot Responses').click(); + cy.url().should('include', 'language-generation'); + cy.findByTestId('LeftNav-CommandBarButtonUser Input').click(); + cy.url().should('include', 'language-understanding'); + cy.findByTestId('LeftNav-CommandBarButtonSettings').click(); + cy.url().should('include', 'setting'); + }); +}); diff --git a/Composer/cypress/integration/LuisDeploy.spec.js b/Composer/cypress/integration/LuisDeploy.spec.ts similarity index 57% rename from Composer/cypress/integration/LuisDeploy.spec.js rename to Composer/cypress/integration/LuisDeploy.spec.ts index d6c3172d06..3283bf6cd6 100644 --- a/Composer/cypress/integration/LuisDeploy.spec.js +++ b/Composer/cypress/integration/LuisDeploy.spec.ts @@ -1,4 +1,5 @@ -/// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. context('Luis Deploy', () => { beforeEach(() => { @@ -11,7 +12,7 @@ context('Luis Deploy', () => { }); it('can deploy luis success', () => { - cy.get('[data-testid="LeftNav-CommandBarButtonUser Input"]').click(); + cy.findByTestId('LeftNav-CommandBarButtonUser Input').click(); cy.route({ method: 'POST', @@ -19,24 +20,22 @@ context('Luis Deploy', () => { status: 200, response: 'fixture:luPublish/success', }); - cy.getByText('Start Bot').click(); - cy.wait(5000); + cy.findByText('Start Bot').click(); + // clear its settings before - cy.get('[data-testid="ProjectNameInput"]') + cy.findByTestId('ProjectNameInput') .clear() .type('MyProject'); - cy.get('[data-testid="EnvironmentInput"]') + cy.findByTestId('EnvironmentInput') .clear() .type('composer'); - cy.get('[data-testid="AuthoringKeyInput"]') + cy.findByTestId('AuthoringKeyInput') .clear() .type('0d4991873f334685a9686d1b48e0ff48'); // wait for the debounce interval of sync settings - cy.wait(1000); - cy.getByText('OK').click(); - cy.wait(1000); - cy.getByText('Restart Bot').should('exist'); - cy.getByText('Test in Emulator').should('exist'); + cy.findByText('OK').click(); + cy.findByText('Restart Bot').should('exist'); + cy.findByText('Test in Emulator').should('exist'); cy.route({ method: 'POST', @@ -44,11 +43,9 @@ context('Luis Deploy', () => { status: 400, 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('OK').click(); + cy.findByText('Restart Bot').click(); + cy.findByText('Try again').click(); + cy.findByTestId('AuthoringKeyInput').type('no-id'); + cy.findByText('OK').click(); }); }); diff --git a/Composer/cypress/integration/NewDialog.spec.js b/Composer/cypress/integration/NewDialog.spec.js deleted file mode 100644 index dedb0be001..0000000000 --- a/Composer/cypress/integration/NewDialog.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -/// - -context('Creating a new Dialog', () => { - beforeEach(() => { - cy.visit(Cypress.env('COMPOSER_URL')); - cy.copyBot('TodoSample', 'ToDoBotCopy'); - cy.get('[data-testid="LeftNav-CommandBarButtonDesign Flow"]').click(); - }); - - it('can create a new dialog from project tree', () => { - cy.getByText('New Dialog ..').click(); - cy.get('input[data-testid="NewDialogName"]').type('__TestNewDialog2'); - cy.get('input[data-testid="NewDialogName"]').type('{enter}'); - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('__TestNewDialog2').should('exist'); - }); - }); -}); diff --git a/Composer/cypress/integration/NewDialog.spec.ts b/Composer/cypress/integration/NewDialog.spec.ts new file mode 100644 index 0000000000..23c379ddfd --- /dev/null +++ b/Composer/cypress/integration/NewDialog.spec.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('Creating a new Dialog', () => { + beforeEach(() => { + cy.visit(Cypress.env('COMPOSER_URL')); + cy.createBot('TodoSample'); + cy.findByTestId('LeftNav-CommandBarButtonDesign Flow').click(); + }); + + it('can create a new dialog from project tree', () => { + cy.findByText('New Dialog ..').click(); + cy.findByTestId('NewDialogName').type('{selectall}__TestNewDialog2{enter}'); + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('__TestNewDialog2').should('exist'); + }); + }); +}); diff --git a/Composer/cypress/integration/NotificationPage.spec.js b/Composer/cypress/integration/NotificationPage.spec.js deleted file mode 100644 index 0618ed70ba..0000000000 --- a/Composer/cypress/integration/NotificationPage.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -/// - -context('check notifications page', () => { - beforeEach(() => { - cy.visit(Cypress.env('COMPOSER_URL')); - cy.createBot('TodoSample'); - }); - - it('can show lg syntax error ', () => { - cy.visitPage("Bot Responses"); - // left nav tree - cy.contains('TodoSample.Main'); - cy.contains('All'); - - cy.get('.toggleEditMode button').click(); - cy.get('textarea').type('test lg syntax error'); - - cy.visitPage("Notifications"); - - cy.get('[data-testid="notifications-table-view"]').within(() => { - cy.getByText('common.lg').should('exist'); - }); - - }); -}); diff --git a/Composer/cypress/integration/NotificationPage.spec.ts b/Composer/cypress/integration/NotificationPage.spec.ts new file mode 100644 index 0000000000..99835edfaa --- /dev/null +++ b/Composer/cypress/integration/NotificationPage.spec.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('Notification Page', () => { + beforeEach(() => { + cy.visit(Cypress.env('COMPOSER_URL')); + cy.createBot('TodoSample'); + cy.visitPage('Notifications'); + }); + + it('can show lg syntax error ', () => { + cy.visitPage('Bot Responses'); + + cy.get('.toggleEditMode button').as('switchButton'); + cy.get('@switchButton').click(); + cy.get('textarea').type('test lg syntax error'); + + cy.visitPage('Notifications'); + + cy.get('[data-testid="notifications-table-view"]').within(() => { + cy.findAllByText('common.lg') + .should('exist') + .first() + .click(); + }); + + cy.findAllByText('Bot Responses').should('exist'); + cy.get('@switchButton').should('be.disabled'); + }); + + it('can show lu syntax error ', () => { + cy.visitPage('User Input'); + + cy.get('.dialogNavTree button[title="__TestTodoSample.Main"]').click({ multiple: true }); + + cy.get('.toggleEditMode button').click(); + cy.get('textarea').type('test lu syntax error'); + + cy.visitPage('Notifications'); + + cy.get('[data-testid="notifications-table-view"]').within(() => { + cy.findAllByText('Main.lu') + .should('exist') + .first() + .click(); + }); + + cy.findAllByText('__TestTodoSample.Main').should('exist'); + }); + + it('can show dialog expression error ', () => { + cy.visitPage('Design Flow'); + + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('Greeting').click(); + }); + + cy.withinEditor('VisualEditor', () => { + cy.findByText('Greeting').should('exist'); + }); + + cy.withinEditor('FormEditor', () => { + cy.findByText('Condition').should('exist'); + cy.get('.ObjectItem input').type('()'); + }); + + cy.visitPage('Notifications'); + + cy.get('[data-testid="notifications-table-view"]').within(() => { + cy.findAllByText('Main.dialog') + .should('exist') + .first() + .click(); + }); + + cy.findAllByText('Greeting').should('exist'); + }); +}); diff --git a/Composer/cypress/integration/Onboarding.spec.js b/Composer/cypress/integration/Onboarding.spec.js deleted file mode 100644 index 595cb0be40..0000000000 --- a/Composer/cypress/integration/Onboarding.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -/// - -context('onboarding', () => { - beforeEach(() => { - cy.visit(`${Cypress.env('COMPOSER_URL')}/home`, { enableOnboarding: true }); - cy.wait(1000); - cy.get('[data-testid="homePage-ToolBar-New"]').within(() => { - cy.getByText('New').click(); - }); - cy.wait(5000); - - cy.get('input[data-testid="Create from template"]').click({ force: true }); - cy.wait(100); - cy.get('[data-testid="TodoSample"]').click(); - cy.wait(100); - cy.get('button[data-testid="NextStepButton"]').click(); - cy.wait(100); - cy.get('input[data-testid="NewDialogName"]').type('__TestOnboarding'); - cy.get('input[data-testid="NewDialogName"]').type('{enter}'); - cy.wait(2000); - - //enable onboarding setting - cy.visitPage("Settings"); - cy.wait(2000); - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.get('[title="Onboarding"]').click(); - }); - cy.get('button[data-testid="onboardingToggle"]').click(); - cy.visitPage("Design Flow"); - }); - - it('walk through product tour teaching bubbles', () => { - cy.getByTestId('onboardingNextSet', { force: true }).click(); - cy.getByTestId('onboardingNext', { force: true }).click(); - cy.getByTestId('onboardingNext', { force: true }).click(); - cy.getByTestId('onboardingNext', { force: true }).click(); - cy.getByTestId('onboardingNext', { force: true }).click(); - cy.getByTestId('onboardingNext', { force: true }).click(); - - cy.getByTestId('onboardingNextSet', { force: true }).click(); - cy.getByTestId('onboardingNext', { force: true }).click(); - - cy.getByTestId('onboardingNextSet', { force: true }).click(); - cy.getByTestId('onboardingNext', { force: true }).click(); - - cy.getByTestId('onboardingNextSet', { force: true }).click(); - cy.getByTestId('onboardingNext', { force: true }).click(); - - cy.getByTestId('onboardingDone', { force: true }).click(); - }); -}); diff --git a/Composer/cypress/integration/Onboarding.spec.ts b/Composer/cypress/integration/Onboarding.spec.ts new file mode 100644 index 0000000000..e6f714fb16 --- /dev/null +++ b/Composer/cypress/integration/Onboarding.spec.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('Onboarding', () => { + beforeEach(() => { + window.localStorage.setItem('composer:OnboardingState', JSON.stringify({ complete: false })); + + cy.visit(`${Cypress.env('COMPOSER_URL')}/home`); + cy.findByTestId('homePage-ToolBar-New').within(() => { + cy.findByText('New').click(); + }); + + cy.findByTestId('Create from template').click({ force: true }); + cy.findByTestId('TodoSample').click(); + cy.findByTestId('NextStepButton').click(); + cy.findByTestId('NewDialogName').type('{selectall}__TestOnboarding{enter}'); + + //enable onboarding setting + cy.visitPage('Settings'); + cy.findByText('Onboarding').click(); + cy.findByTestId('onboardingToggle').click(); + cy.visitPage('Design Flow'); + }); + + it('walk through product tour teaching bubbles', () => { + cy.findByTestId('onboardingNextSet').click(); + cy.findByTestId('onboardingNext').click(); + cy.findByTestId('onboardingNext').click(); + cy.findByTestId('onboardingNext').click(); + cy.findByTestId('onboardingNext').click(); + cy.findByTestId('onboardingNext').click(); + + cy.findByTestId('onboardingNextSet').click(); + cy.findByTestId('onboardingNext').click(); + + cy.findByTestId('onboardingNextSet').click(); + cy.findByTestId('onboardingNext').click(); + + cy.findByTestId('onboardingNextSet').click(); + cy.findByTestId('onboardingNext').click(); + + cy.findByTestId('onboardingDone').click(); + }); +}); diff --git a/Composer/cypress/integration/RemoveDialog.spec.js b/Composer/cypress/integration/RemoveDialog.spec.ts similarity index 52% rename from Composer/cypress/integration/RemoveDialog.spec.js rename to Composer/cypress/integration/RemoveDialog.spec.ts index 16f8faa26a..cbe924757e 100644 --- a/Composer/cypress/integration/RemoveDialog.spec.js +++ b/Composer/cypress/integration/RemoveDialog.spec.ts @@ -1,26 +1,27 @@ -/// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. context('RemoveDialog', () => { beforeEach(() => { cy.visit(Cypress.env('COMPOSER_URL')); - cy.copyBot('ToDoBotWithLuisSample', 'ToDoBotWithLuisSampleSpec'); + cy.createBot('ToDoBotWithLuisSample'); }); it('can remove dialog', () => { - cy.getByTestId('ProjectTree').within(() => { - cy.getByTestId('DialogTreeItemtriggers[4]').within(() => { - cy.getByTestId('dialogMoreButton') + cy.findByTestId('ProjectTree').within(() => { + cy.findByTestId('DialogTreeItemtriggers[4]').within(() => { + cy.findByTestId('dialogMoreButton') .first() .invoke('attr', 'style', 'visibility: visible') .click(); - }); }); + }); cy.get('.ms-ContextualMenu-linkContent > .ms-ContextualMenu-itemText').within(() => { - cy.getByText('Delete').click(); + cy.findByText('Delete').click(); }); - cy.getByTestId('ProjectTree').within(() => { + cy.findByTestId('ProjectTree').within(() => { cy.get('[title="AddItem"]').should('not.exist'); }); }); diff --git a/Composer/cypress/integration/SaveAs.spec.js b/Composer/cypress/integration/SaveAs.spec.js deleted file mode 100644 index bd71d8a748..0000000000 --- a/Composer/cypress/integration/SaveAs.spec.js +++ /dev/null @@ -1,22 +0,0 @@ -/// - -context('Saving As', () => { - beforeEach(() => { - cy.visit(Cypress.env('COMPOSER_URL')); - }); - - it('can create a new bot from an existing bot', () => { - cy.createBot('ToDoBotWithLuisSample'); - cy.get('[data-testid="LeftNav-CommandBarButtonHome"]').click(); - cy.getByText('Save as').click(); - - cy.get('input[data-testid="NewDialogName"]').type('__TestSaveAs'); - cy.get('input[data-testid="NewDialogName"]').type('{enter}'); - cy.wait(1000); - - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('__TestSaveAs.Main').should('exist'); - cy.getByText('ViewCollection').should('exist'); - }); - }); -}); diff --git a/Composer/cypress/integration/SaveAs.spec.ts b/Composer/cypress/integration/SaveAs.spec.ts new file mode 100644 index 0000000000..300776f0db --- /dev/null +++ b/Composer/cypress/integration/SaveAs.spec.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('Saving As', () => { + beforeEach(() => { + cy.visit(Cypress.env('COMPOSER_URL')); + cy.createBot('ToDoBotWithLuisSample'); + }); + + it('can create a new bot from an existing bot', () => { + cy.findByTestId('LeftNav-CommandBarButtonHome').click(); + cy.findByText('Save as').click(); + + cy.findByTestId('NewDialogName').type('{selectall}__TestSaveAs{enter}'); + + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('__TestSaveAs.Main').should('exist'); + cy.findByText('ViewCollection').should('exist'); + }); + }); +}); diff --git a/Composer/cypress/integration/SwitchCondition.spec.js b/Composer/cypress/integration/SwitchCondition.spec.js deleted file mode 100644 index 8a98afc186..0000000000 --- a/Composer/cypress/integration/SwitchCondition.spec.js +++ /dev/null @@ -1,164 +0,0 @@ -/// - -// this test is too unstable right now -// re-enable when stablized -context.skip('SwitchCondition', () => { - beforeEach(() => { - cy.visit(Cypress.env('COMPOSER_URL')); - cy.startFromTemplate('EmptyBot', 'SwitchConditionSpec'); - }); - - //will remove skip after add trigger is ok - it('can manage cases', () => { - cy.addEventHandler('Handle Unknown Intent'); - - cy.withinEditor('VisualEditor', () => { - cy.getByText(/OnUnknownIntent/).click({ force: true }); - cy.wait(100); - cy.getByText(/UnknownIntent/).click({ force: true }); - cy.wait(100); - cy.getByTestId('StepGroupAdd').click({ force: true }); - cy.getByText('Flow').click({ force: true }); - cy.getByText('Branch: Switch').click({ force: true }); - cy.getByTestId('SwitchConditionDiamond').click({ force: true }); - }); - - // Add case and add/delete/edit steps - cy.withinEditor('FormEditor', () => { - // Edit condition - cy.getByLabelText('Condition').type('user.age >= 21'); - - // Add new case - cy.getByText('Add New Case').click({ force: true }); - cy.getByLabelText('Value') - .type('Case1') - .type('{enter}'); - - // Add some steps - - // Send activity - // Use { force: true } can disable error checking like dom not visible or width and height '0 * 0' pixels. - // So if a button is in a popup window, using { force: true } to button click can make the tests more stable. - cy.getByText('Add New Action for Case1').click({ force: true }); - cy.getByText('Send Messages').click({ force: true }); - cy.getByText('Send an Activity').click({ force: true }); - cy.wait(300); - }); - cy.withinEditor('VisualEditor', () => { - cy.getByText('Branch: Switch').click({ force: true }); - }); - cy.withinEditor('FormEditor', () => { - // Edit array - cy.getByText('Add New Action for Case1').click({ force: true }); - cy.getByText('Memory manipulation').click({ force: true }); - cy.getByText('Edit an Array Property').click({ force: true }); - cy.wait(300); - }); - cy.withinEditor('VisualEditor', () => { - cy.getByText('Branch: Switch').click({ force: true }); - }); - cy.withinEditor('FormEditor', () => { - // Log step - cy.getByText('Add New Action for Case1').click({ force: true }); - cy.getByText('Debugging').click({ force: true }); - cy.getByText('Log to console').click({ force: true }); - cy.wait(300); - }); - cy.withinEditor('VisualEditor', () => { - cy.getByText('Branch: Switch').click({ force: true }); - }); - cy.withinEditor('FormEditor', () => { - cy.get('[data-automationid="DetailsRow"]') - .as('steps') - .should('have.length', 3); - - // re-order steps - const btn0 = cy - .get('@steps') - .eq(0) - .find('button') - .click({ force: true }); - btn0.invoke('attr', 'aria-owns').then(menuId => { - cy.get(`#${menuId}`) - .getByText('Move Down') - .click({ force: true }); - cy.wait(100); - }); - - const btn2 = cy - .get('@steps') - .eq(2) - .find('button') - .click({ force: true }); - btn2.invoke('attr', 'aria-owns').then(menuId => { - cy.get(`#${menuId}`) - .getByText('Move Up') - .click({ force: true }); - cy.wait(100); - }); - - // assert that the steps are in correct order - cy.get('@steps') - .get('[data-automationid="DetailsRowCell"][data-automation-key="name"]') - .eq(0) - .should('contain.text', 'Edit an Array Property'); - cy.get('@steps') - .get('[data-automationid="DetailsRowCell"][data-automation-key="name"]') - .eq(1) - .should('contain.text', 'Log to console'); - cy.get('@steps') - .get('[data-automationid="DetailsRowCell"][data-automation-key="name"]') - .eq(2) - .should('contain.text', 'Send an Activity'); - - // Add another new case - cy.getByText('Add New Case').click({ force: true }); - cy.wait(100); - cy.getByLabelText('Value') - .type('Case2') - .type('{enter}'); - - cy.wait(100); - - // move first case - let btn = cy - .get('.CasesFieldConditionsMenu') - .first() - .find('button'); - btn.click({ force: true }); - btn.invoke('attr', 'aria-owns').then(menuId => { - cy.get(`#${menuId}`) - .getByText('Move Down') - .click({ force: true }); - cy.wait(100); - }); - - cy.get('[role="separator"]') - .filter(':not(:contains(Branch: Switch))') - .should('have.length', 3) - .eq(1) - .should('have.text', 'Branch: Case1'); - - cy.wait(100); - - // remove case1 - btn = cy - .get('.CasesFieldConditionsMenu') - .first() - .find('button'); - btn.click({ force: true }); - btn.invoke('attr', 'aria-owns').then(menuId => { - cy.get(`#${menuId}`) - .getByText('Remove') - .click({ force: true }); - cy.wait(100); - }); - - cy.get('[role="separator"]') - .filter(':not(:contains(Branch: Switch))') - .should('have.length', 2) - .eq(1) - .should('have.text', 'Default'); - }); - }); -}); diff --git a/Composer/cypress/integration/ToDoBot.spec.js b/Composer/cypress/integration/ToDoBot.spec.js deleted file mode 100644 index 13d349d89d..0000000000 --- a/Composer/cypress/integration/ToDoBot.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -/// - -context('ToDo Bot', () => { - beforeEach(() => { - cy.visit(Cypress.env('COMPOSER_URL')); - cy.createBot('TodoSample'); - }); - - it('can open the main dialog', () => { - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('__TestTodoSample.Main').click(); - cy.wait(100); - }); - cy.withinEditor('FormEditor', () => { - cy.getByDisplayValue('__TestTodoSample').should('exist'); - }); - }); - - it('can open the AddToDo dialog', () => { - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('AddToDo').click(); - cy.wait(100); - }); - - cy.withinEditor('FormEditor', () => { - cy.getByDisplayValue('AddToDo').should('exist'); - }); - }); - - it('can open the ClearToDos dialog', () => { - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('ClearToDos').click(); - cy.wait(100); - }); - - cy.withinEditor('FormEditor', () => { - cy.getByDisplayValue('ClearToDos').should('exist'); - }); - }); - - it('can open the DeleteToDo dialog', () => { - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('DeleteToDo').click(); - cy.wait(100); - }); - - cy.withinEditor('FormEditor', () => { - cy.getByDisplayValue('DeleteToDo').should('exist'); - }); - }); - - it('can open the ShowToDos dialog', () => { - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('ShowToDos').click(); - cy.wait(100); - }); - - cy.withinEditor('FormEditor', () => { - cy.getByDisplayValue('ShowToDos').should('exist'); - }); - }); -}); diff --git a/Composer/cypress/integration/ToDoBot.spec.ts b/Composer/cypress/integration/ToDoBot.spec.ts new file mode 100644 index 0000000000..6af6943b15 --- /dev/null +++ b/Composer/cypress/integration/ToDoBot.spec.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('ToDo Bot', () => { + beforeEach(() => { + cy.visit(Cypress.env('COMPOSER_URL')); + cy.createBot('TodoSample'); + }); + + it('can open the main dialog', () => { + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('__TestTodoSample.Main').click(); + }); + cy.withinEditor('FormEditor', () => { + cy.findByDisplayValue('__TestTodoSample').should('exist'); + }); + }); + + it('can open the AddToDo dialog', () => { + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('AddToDo').click(); + }); + + cy.withinEditor('FormEditor', () => { + cy.findByDisplayValue('AddToDo').should('exist'); + }); + }); + + it('can open the ClearToDos dialog', () => { + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('ClearToDos').click(); + }); + + cy.withinEditor('FormEditor', () => { + cy.findByDisplayValue('ClearToDos').should('exist'); + }); + }); + + it('can open the DeleteToDo dialog', () => { + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('DeleteToDo').click(); + }); + + cy.withinEditor('FormEditor', () => { + cy.findByDisplayValue('DeleteToDo').should('exist'); + }); + }); + + it('can open the ShowToDos dialog', () => { + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('ShowToDos').click(); + }); + + cy.withinEditor('FormEditor', () => { + cy.findByDisplayValue('ShowToDos').should('exist'); + }); + }); +}); diff --git a/Composer/cypress/integration/VisualDesigner.spec.js b/Composer/cypress/integration/VisualDesigner.spec.js deleted file mode 100644 index 06f2e5a759..0000000000 --- a/Composer/cypress/integration/VisualDesigner.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -/// - -context('Visual Designer', () => { - before(() => { - cy.visit(Cypress.env('COMPOSER_URL')); - cy.createBot('TodoSample'); - cy.wait(100); - }); - - beforeEach(() => { - // Return to Main.dialog - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('__TestTodoSample.Main').click(); - cy.wait(100); - }); - }); - - it('can find Visual Designer default trigger in container', () => { - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('Greeting (ConversationUpdate)').click(); - cy.wait(500); - }); - - cy.withinEditor('VisualEditor', () => { - cy.getByText('Trigger').should('exist'); - }); - }); -}); diff --git a/Composer/cypress/integration/VisualDesigner.spec.ts b/Composer/cypress/integration/VisualDesigner.spec.ts new file mode 100644 index 0000000000..eb6ee33041 --- /dev/null +++ b/Composer/cypress/integration/VisualDesigner.spec.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +context('Visual Designer', () => { + beforeEach(() => { + cy.visit(Cypress.env('COMPOSER_URL')); + cy.createBot('TodoSample'); + // Return to Main.dialog + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('__TestTodoSample.Main').click(); + }); + }); + + it('can find Visual Designer default trigger in container', () => { + cy.findByTestId('ProjectTree').within(() => { + cy.findByText('Greeting').click(); + }); + + cy.withinEditor('VisualEditor', () => { + cy.findByText('ConversationUpdate activity').should('exist'); + }); + }); +}); diff --git a/Composer/cypress/integration/homePage.spec.js b/Composer/cypress/integration/homePage.spec.js deleted file mode 100644 index 15fddc136b..0000000000 --- a/Composer/cypress/integration/homePage.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -/// -context('check Nav Expandion ', () => { - beforeEach(() => { - cy.visit(Cypress.env('COMPOSER_URL')); - }); - it('can open buttons in home page', () => { - cy.get('[data-testid="LeftNav-CommandBarButtonHome"]').click(); - cy.get('[data-testid="homePage-ToolBar-New"]').click(); - cy.getByText('Create from scratch?').should('exist'); - cy.getByText('Cancel').should('exist'); - cy.getByText('Cancel').click(); - cy.get('[data-testid="homePage-ToolBar-Open"]').click(); - cy.getByText('Select a Bot').should('exist'); - cy.getByText('Cancel').should('exist'); - cy.getByText('Cancel').click(); - cy.get('[data-testid="homePage-body-New"]').click(); - cy.getByText('Create from scratch?').should('exist'); - }); -}); diff --git a/Composer/cypress/plugins/cy-ts-preprocessor.js b/Composer/cypress/plugins/cy-ts-preprocessor.js new file mode 100644 index 0000000000..149b4a8052 --- /dev/null +++ b/Composer/cypress/plugins/cy-ts-preprocessor.js @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-var-requires */ + +const wp = require('@cypress/webpack-preprocessor'); + +const webpackOptions = { + resolve: { + extensions: ['.ts', '.js'], + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: [/node_modules/], + use: [ + { + loader: 'ts-loader', + }, + ], + }, + ], + }, +}; + +const options = { + webpackOptions, +}; + +module.exports = wp(options); diff --git a/Composer/cypress/plugins/index.js b/Composer/cypress/plugins/index.js index fd170fba69..e107f2de30 100644 --- a/Composer/cypress/plugins/index.js +++ b/Composer/cypress/plugins/index.js @@ -1,17 +1,10 @@ -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) +/* eslint-disable @typescript-eslint/no-var-requires */ -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -} +const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor'); + +module.exports = on => { + on('file:preprocessor', cypressTypeScriptPreprocessor); +}; diff --git a/Composer/cypress/support/commands.d.ts b/Composer/cypress/support/commands.d.ts new file mode 100644 index 0000000000..d9e9e141b4 --- /dev/null +++ b/Composer/cypress/support/commands.d.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/// + +declare namespace Cypress { + interface Chainable { + /** + * Creates a bot based on template id. + * If botName not provided, names the bot __Test${botId}, + * otherwise, __Test&{botName}. + * @example cy.createBot('TodoSample', 'NewBotName') + */ + createBot(botId: string, botName?: string): void; + + /** + * Visits a page from the left nav bar using the page's testid + * @example visitPage('Bot Responses'); + */ + visitPage(page: string): void; + + /** + * Invokes callback inside editor context + * @example cy.withinEditor('VisualEditor', () => { + * cy.findByText('SomeText'); + * }); + */ + withinEditor(editor: string, cb: (currentSubject: JQuery) => void): void; + } +} diff --git a/Composer/cypress/support/commands.js b/Composer/cypress/support/commands.js deleted file mode 100644 index a82d72318c..0000000000 --- a/Composer/cypress/support/commands.js +++ /dev/null @@ -1,107 +0,0 @@ -// *********************************************** This example commands.js -// shows you how to create various custom commands and overwrite existing -// commands. -// -// For more comprehensive examples of custom commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- Cypress.Commands.add("login", (email, -// password) => { ... }) -// -// -// -- This is a child command -- Cypress.Commands.add("drag", { prevSubject: -// 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- Cypress.Commands.add("dismiss", { prevSubject: -// 'optional'}, (subject, options) => { ... }) -// -// - -Cypress.Commands.overwrite("visit", (originalFn, url, { enableOnboarding } = {}) => { - if (!enableOnboarding) { - cy.window().then(window => window.localStorage.setItem('composer:OnboardingState', JSON.stringify({ complete: true }))); - } - originalFn(url); - }); - -import 'cypress-testing-library/add-commands'; - -Cypress.Commands.add('createBot', botName => { - cy.get('[data-testid="LeftNav-CommandBarButtonHome"]').click(); - cy.wait(500); - cy.get('[data-testid="homePage-ToolBar-New"]').within(() => { - cy.getByText('New').click(); - }); - cy.wait(500); - cy.get('input[data-testid="Create from template"]').click({ force: true }); - cy.wait(100); - cy.get(`[data-testid=${botName}]`).click(); - cy.wait(100); - cy.get('button[data-testid="NextStepButton"]').click(); - cy.wait(100); - cy.get('input[data-testid="NewDialogName"]').type(`__Test${botName}`); - cy.get('input[data-testid="NewDialogName"]').type('{enter}'); -}); - -Cypress.Commands.add('openBot', botName => { - cy.get('[data-testid="LeftNav-CommandBarButtonHome"]').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); - }); - cy.wait(500); -}); - -Cypress.Commands.add('withinEditor', (editorName, cb) => { - cy.get(`iframe[name="${editorName}"]`).then(editor => { - cy.wrap(editor.contents().find('body')).within(cb); - }); -}); - -Cypress.Commands.add('openDialog', dialogName => { - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText(dialogName).click(); - cy.wait(500); - }); -}); - -Cypress.Commands.add('startFromTemplate', (template, name) => { - cy.get('[data-testid="LeftNav-CommandBarButtonHome"]').click(); - cy.getByTestId(`TemplateCopy-${template}`).click(); - cy.get('input[data-testid="NewDialogName"]').type(`__Test${name}`); - cy.get('input[data-testid="NewDialogName"]').type('{enter}'); - cy.wait(1000); -}); - -Cypress.Commands.add('copyBot', (bot, name) => { - cy.createBot(bot); - cy.get('[data-testid="LeftNav-CommandBarButtonHome"]').click(); - cy.getByText('Save as').click(); - - cy.get('input[data-testid="NewDialogName"]').type(`__Test${name}`); - cy.get('input[data-testid="NewDialogName"]').type('{enter}'); - cy.wait(1000); -}); - -Cypress.Commands.add('addEventHandler', handler => { - cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText(/New Trigger ../).click(); - }); - cy.get(`[data-testid="triggerTypeDropDown"]`).click(); - cy.getByText(handler).click(); - if (handler === 'Dialog trigger') { - cy.get(`[data-testid="eventTypeDropDown"]`).click(); - cy.getByText('consultDialog').click(); - } - cy.get(`[data-testid="triggerFormSubmit"]`).click(); -}); - -Cypress.Commands.add('visitPage', (page) => { - cy.get(`[data-testid="LeftNav-CommandBarButton${page}"]`).click(); -}); diff --git a/Composer/cypress/support/commands.ts b/Composer/cypress/support/commands.ts new file mode 100644 index 0000000000..60b0e246d9 --- /dev/null +++ b/Composer/cypress/support/commands.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import '@testing-library/cypress/add-commands'; + +Cypress.Commands.add('createBot', (bobotId: string, botName?: string) => { + cy.findByTestId('LeftNav-CommandBarButtonHome').click(); + cy.findByTestId('homePage-ToolBar-New').within(() => { + cy.findByText('New').click(); + }); + cy.findByTestId('Create from template').click({ force: true }); + cy.findByTestId(`${bobotId}`).click(); + cy.findByTestId('NextStepButton').click(); + cy.findByTestId('NewDialogName').type(`{selectall}__Test${botName || bobotId}{enter}`); + cy.wait(1000); +}); + +Cypress.Commands.add('withinEditor', (editorName, cb) => { + cy.get(`iframe[name="${editorName}"]`).then(editor => { + cy.wrap(editor.contents().find('body') as JQuery).within(cb); + }); +}); + +Cypress.Commands.add('visitPage', page => { + cy.findByTestId(`LeftNav-CommandBarButton${page}`).click(); + cy.wait(3000); +}); + +Cypress.on('uncaught:exception', (err, runnable) => { + console.log('uncaught exception', err); + return false; +}); diff --git a/Composer/cypress/support/index.js b/Composer/cypress/support/index.js deleted file mode 100644 index 5c76e07023..0000000000 --- a/Composer/cypress/support/index.js +++ /dev/null @@ -1,29 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands'; - -// Alternatively you can use CommonJS syntax: -// require('./commands') - -beforeEach(() => { - cy.exec('yarn test:integration:clean'); -}); - -after(() => { - cy.wait(500); - cy.exec('yarn test:integration:clean'); -}); diff --git a/Composer/cypress/support/index.ts b/Composer/cypress/support/index.ts new file mode 100644 index 0000000000..0bbb874401 --- /dev/null +++ b/Composer/cypress/support/index.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import './commands'; + +beforeEach(() => { + cy.exec('yarn test:integration:clean'); + window.localStorage.setItem('composer:OnboardingState', JSON.stringify({ complete: true })); +}); + +after(() => { + cy.wait(500); + cy.exec('yarn test:integration:clean'); +}); diff --git a/Composer/cypress/tsconfig.json b/Composer/cypress/tsconfig.json new file mode 100644 index 0000000000..6a00ad6e70 --- /dev/null +++ b/Composer/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "baseUrl": "../node_modules", + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "@types/testing-library__cypress", "./support/commands"] + }, + "include": ["**/*.ts"] +} diff --git a/Composer/jest.config.js b/Composer/jest.config.js index 32b10b6b50..da2165fe69 100644 --- a/Composer/jest.config.js +++ b/Composer/jest.config.js @@ -41,5 +41,6 @@ module.exports = { '/packages/extensions/visual-designer', '/packages/lib/code-editor', '/packages/lib/shared', + '/packages/tools/language-servers/language-generation', ], }; diff --git a/Composer/package.json b/Composer/package.json index 4901453bb5..58cde06e03 100644 --- a/Composer/package.json +++ b/Composer/package.json @@ -1,27 +1,35 @@ { "name": "@bfc/root", + "license": "MIT", "private": true, "resolutions": { "@types/react": "16.9.0" }, + "engines": { + "node": ">=12" + }, "workspaces": [ "packages/client", "packages/server", "packages/extensions", "packages/extensions/*", "packages/lib", - "packages/lib/*" + "packages/lib/*", + "packages/tools", + "packages/tools/language-servers", + "packages/tools/language-servers/*" ], "scripts": { - "build": "node scripts/begin.js && yarn build:prod", - "build:prod": "yarn build:lib && yarn build:extensions && yarn build:server && yarn build:client", - "build:dev": "yarn build:lib && yarn build:extensions", + "build": "node scripts/update.js && node scripts/begin.js && yarn build:prod", + "build:prod": "yarn build:dev && yarn build:server && yarn build:client", + "build:dev": "yarn build:tools && yarn build:lib && yarn build:extensions", "build:lib": "yarn workspace @bfc/libs build:all", "build:extensions": "yarn workspace @bfc/extensions build:all", "build:server": "yarn workspace @bfc/server build", "build:client": "yarn workspace @bfc/client build", + "build:tools": "yarn workspace @bfc/tools build:all", "start": "cross-env NODE_ENV=production PORT=3000 yarn start:server", - "startall": "concurrently --kill-others-on-fail \"npm:runtime\" \"npm:start\"", + "startall": "node scripts/update.js && concurrently --kill-others-on-fail \"npm:runtime\" \"npm:start\"", "start:dev": "concurrently \"npm:start:client\" \"npm:start:server:dev\"", "start:client": "yarn workspace @bfc/client start", "start:server": "yarn workspace @bfc/server start", @@ -31,7 +39,7 @@ "test:coverage": "yarn test --coverage --no-cache --reporters=default", "test:integration": "cypress run --browser chrome", "test:integration:open": "cypress open", - "test:integration:clean": "rimraf ../MyBots/__Test* packages/server/data.json", + "test:integration:clean": "rimraf ../MyBots/__Test*", "lint": "wsrun --exclude-missing --collect-logs --report lint", "lint:fix": "wsrun --exclude-missing --collect-logs --report lint:fix", "typecheck": "concurrently --kill-others-on-fail \"npm:typecheck:*\"", @@ -59,28 +67,29 @@ "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.3.3", "@bfc/eslint-plugin-bfcomposer": "*", + "@cypress/webpack-preprocessor": "^4.1.1", "@emotion/babel-preset-css-prop": "^10.0.17", - "@typescript-eslint/eslint-plugin": "2.6.0", - "@typescript-eslint/parser": "2.6.0", + "@testing-library/cypress": "^5.0.2", + "@typescript-eslint/eslint-plugin": "2.10.0", + "@typescript-eslint/parser": "2.10.0", "babel-jest": "24.0.0", "concurrently": "^4.1.0", "coveralls": "^3.0.7", "cypress": "^3.6.1", "cypress-plugin-tab": "^1.0.1", - "cypress-testing-library": "^3.0.1", - "eslint": "^5.15.1", - "eslint-config-prettier": "^4.1.0", + "eslint": "^6.7.2", + "eslint-config-prettier": "^6.7.0", + "eslint-plugin-cypress": "^2.7.0", "eslint-plugin-emotion": "^10.0.14", "eslint-plugin-format-message": "^6.2.3", - "eslint-plugin-import": "^2.16.0", - "eslint-plugin-jsx-a11y": "6.1.2", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-jsx-a11y": "^6.1.3", "eslint-plugin-lodash": "^6.0.0", - "eslint-plugin-notice": "^0.7.8", - "eslint-plugin-prettier": "^3.0.1", - "eslint-plugin-react": "7.12.4", - "eslint-plugin-react-hooks": "^1.6.0", + "eslint-plugin-notice": "^0.8.9", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-react": "^7.17.0", + "eslint-plugin-react-hooks": "^2.3.0", "eslint-plugin-security": "^1.4.0", - "eslint-plugin-typescript": "^0.14.0", "husky": "^1.3.1", "jest": "24.0.0", "jest-dom": "^3.1.3", @@ -89,11 +98,12 @@ "mocha": "5.2.0", "mocha-junit-reporter": "^1.22.0", "mocha-multi-reporters": "^1.1.7", - "prettier": "^1.15.3", + "prettier": "^1.19.1", "react-testing-library": "^6.0.2", "rimraf": "^2.6.3", - "typescript": "3.6.4", - "wsrun": "^3.6.4" + "ts-loader": "^6.2.1", + "typescript": "3.7.2", + "wsrun": "^5.1.0" }, "dependencies": { "cross-env": "^6.0.3" diff --git a/Composer/packages/client/__tests__/components/DialogWrapper/index.test.tsx b/Composer/packages/client/__tests__/components/DialogWrapper/index.test.tsx new file mode 100644 index 0000000000..6735db19f0 --- /dev/null +++ b/Composer/packages/client/__tests__/components/DialogWrapper/index.test.tsx @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; +import { render } from 'react-testing-library'; +import { DialogWrapper } from '@src/components/DialogWrapper'; + +describe('', () => { + const props = { + isOpen: true, + title: 'My Dialog', + subText: 'Create new dialog', + onDismiss: jest.fn(), + }; + + it('renders null when not open', () => { + const { container } = render(); + expect(container.hasChildNodes()).toBeFalsy(); + }); +}); diff --git a/Composer/packages/client/__tests__/components/notificationHeader.test.js b/Composer/packages/client/__tests__/components/notificationHeader.test.js index 182299bdd8..3664a47724 100644 --- a/Composer/packages/client/__tests__/components/notificationHeader.test.js +++ b/Composer/packages/client/__tests__/components/notificationHeader.test.js @@ -7,17 +7,16 @@ import { fireEvent, render } from 'react-testing-library'; import { NotificationHeader } from '../../src/pages/notifications/NotificationHeader'; describe('', () => { - const items = ['test1', 'test2', 'test3']; it('should render the NotificationHeader', () => { const mockOnChange = jest.fn(() => null); - const { container } = render(); + const { container } = render(); expect(container).toHaveTextContent('Notifications'); expect(container).toHaveTextContent('All'); const dropdown = container.querySelector('[data-testid="notifications-dropdown"]'); fireEvent.click(dropdown); const test = document.querySelector('.ms-Dropdown-callout'); - expect(test).toHaveTextContent('test1'); - expect(test).toHaveTextContent('test2'); + expect(test).toHaveTextContent('Error'); + expect(test).toHaveTextContent('Warning'); }); }); diff --git a/Composer/packages/client/__tests__/components/notificationList.test.js b/Composer/packages/client/__tests__/components/notificationList.test.js index 32fddf9733..347e8e7453 100644 --- a/Composer/packages/client/__tests__/components/notificationList.test.js +++ b/Composer/packages/client/__tests__/components/notificationList.test.js @@ -3,14 +3,36 @@ import * as React from 'react'; import { render } from 'react-testing-library'; +import formatMessage from 'format-message'; import { NotificationList } from '../../src/pages/notifications/NotificationList'; describe('', () => { const items = [ - { type: 'Error', location: 'test1', message: 'error1' }, - { type: 'Warning', location: 'test2', message: 'error2' }, - { type: 'Error', location: 'test3', message: 'error3' }, + { + id: 'Main.dialog', + severity: formatMessage('Error'), + type: 'dialog', + location: formatMessage('test1'), + message: formatMessage('error1'), + diagnostic: '', + }, + { + id: 'Main.lu', + severity: formatMessage('Warning'), + type: 'lu', + location: formatMessage('test2'), + message: formatMessage('error2'), + diagnostic: '', + }, + { + id: 'common.lg', + severity: formatMessage('Error'), + type: 'lg', + location: formatMessage('test3'), + message: formatMessage('error3'), + diagnostic: '', + }, ]; it('should render the NotificationList', () => { const { container } = render(); diff --git a/Composer/packages/client/__tests__/jest.d.ts b/Composer/packages/client/__tests__/jest.d.ts new file mode 100644 index 0000000000..2dcab5485f --- /dev/null +++ b/Composer/packages/client/__tests__/jest.d.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ActionTypes } from '@src/constants'; + +declare global { + namespace jest { + interface Matchers { + toBeDispatchedWith(type: ActionTypes, payload?: any, error?: any); + } + } +} diff --git a/Composer/packages/client/__tests__/setupTests.ts b/Composer/packages/client/__tests__/setupTests.ts new file mode 100644 index 0000000000..f5588cb73e --- /dev/null +++ b/Composer/packages/client/__tests__/setupTests.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import formatMessage from 'format-message'; +import { setIconOptions } from 'office-ui-fabric-react/lib/Styling'; +import 'jest-dom/extend-expect'; +import { cleanup } from 'react-testing-library'; + +// Suppress icon warnings. +setIconOptions({ + disableWarnings: true, +}); + +formatMessage.setup({ + missingTranslation: 'ignore', +}); + +expect.extend({ + toBeDispatchedWith(dispatch: jest.Mock, type: string, payload: any, error?: any) { + if (this.isNot) { + expect(dispatch).not.toHaveBeenCalledWith({ + type, + payload, + error, + }); + } else { + expect(dispatch).toHaveBeenCalledWith({ + type, + payload, + error, + }); + } + + return { + pass: !this.isNot, + message: () => 'dispatch called with correct type and payload', + }; + }, +}); + +afterEach(cleanup); diff --git a/Composer/packages/client/__tests__/store/actions/storage.test.ts b/Composer/packages/client/__tests__/store/actions/storage.test.ts new file mode 100644 index 0000000000..ef7430f7cb --- /dev/null +++ b/Composer/packages/client/__tests__/store/actions/storage.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import httpClient from '@src/utils/httpUtil'; +import { ActionTypes } from '@src/constants'; +import { fetchFolderItemsByPath } from '@src/store/action/storage'; +import { Store } from '@src/store/types'; + +jest.mock('@src/utils/httpUtil'); + +const dispatch = jest.fn(); + +const store = ({ dispatch, getState: () => ({}) } as unknown) as Store; + +describe('fetchFolderItemsByPath', () => { + const id = 'default'; + const path = '/some/path'; + + it('dispatches SET_STORAGEFILE_FETCHING_STATUS', async () => { + await fetchFolderItemsByPath(store, id, path); + + expect(dispatch).toBeDispatchedWith(ActionTypes.SET_STORAGEFILE_FETCHING_STATUS, { + status: 'pending', + }); + }); + + it('fetches folder items from api', async () => { + await fetchFolderItemsByPath(store, id, path); + + expect(httpClient.get).toHaveBeenCalledWith(`/storages/${id}/blobs`, { params: { path } }); + }); + + describe('when api call is successful', () => { + beforeEach(() => { + (httpClient.get as jest.Mock).mockResolvedValue({ some: 'response' }); + }); + + it('dispatches GET_STORAGEFILE_SUCCESS', async () => { + await fetchFolderItemsByPath(store, id, path); + + expect(dispatch).toBeDispatchedWith(ActionTypes.GET_STORAGEFILE_SUCCESS, { + response: { some: 'response' }, + }); + }); + }); + + describe('when api call fails', () => { + beforeEach(() => { + (httpClient.get as jest.Mock).mockRejectedValue('some error'); + }); + + it('dispatches SET_STORAGEFILE_FETCHING_STATUS', async () => { + await fetchFolderItemsByPath(store, id, path); + + expect(dispatch).toBeDispatchedWith( + ActionTypes.SET_STORAGEFILE_FETCHING_STATUS, + { + status: 'failure', + }, + 'some error' + ); + }); + }); +}); diff --git a/Composer/packages/client/config/env.js b/Composer/packages/client/config/env.js index b64938b32a..6b8a48c3ad 100644 --- a/Composer/packages/client/config/env.js +++ b/Composer/packages/client/config/env.js @@ -42,7 +42,7 @@ dotenvFiles.forEach(dotenvFile => { function getGitSha() { try { - const sha = execSync('git rev-parse --short master'); + const sha = execSync('git rev-parse --short', { stdio: ['ignore', 'ignore', 'ignore'] }); return sha; } catch (e) { return 'test'; diff --git a/Composer/packages/client/config/webpack.config.js b/Composer/packages/client/config/webpack.config.js index 0a98e430f2..f56da750fe 100644 --- a/Composer/packages/client/config/webpack.config.js +++ b/Composer/packages/client/config/webpack.config.js @@ -228,6 +228,10 @@ module.exports = function(webpackEnv) { // `web` extension prefixes have been added for better support // for React Native Web. extensions: paths.moduleFileExtensions.map(ext => `.${ext}`), + alias: { + // Support lsp code editor + vscode: require.resolve('monaco-languageclient/lib/vscode-compatibility'), + }, plugins: [ // Adds support for installing with Plug'n'Play, leading to faster installs and adding // guards against forgotten dependencies and such. @@ -402,7 +406,7 @@ module.exports = function(webpackEnv) { plugins: [ new MonacoWebpackPlugin({ // available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options - languages: ['markdown', 'botbuilderlg', 'json'], + languages: ['markdown', 'json'], }), // Generates an `index.html` file with the