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