first commit bois
Some checks failed
release / begin-notification (push) Has been cancelled
release / build-linux (push) Has been cancelled
release / build-msi-installer (push) Has been cancelled
release / build-mac-installer (push) Has been cancelled
release / upload-to-s3 (push) Has been cancelled
release / github-release (push) Has been cancelled
release / end-notification (push) Has been cancelled
Some checks failed
release / begin-notification (push) Has been cancelled
release / build-linux (push) Has been cancelled
release / build-msi-installer (push) Has been cancelled
release / build-mac-installer (push) Has been cancelled
release / upload-to-s3 (push) Has been cancelled
release / github-release (push) Has been cancelled
release / end-notification (push) Has been cancelled
This commit is contained in:
commit
057377207a
34
.config/notice-file/Readme.md
Normal file
34
.config/notice-file/Readme.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Notice.txt File Configuration
|
||||||
|
|
||||||
|
We are automatically generating Notice.txt by using first-level dependencies of the project. The related pipeline uses `config.yaml` stored in this folder.
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Sample:
|
||||||
|
|
||||||
|
```
|
||||||
|
title: "Mattermost Desktop"
|
||||||
|
copyright: "© 2016-2017 Mattermost, Inc. All Rights Reserved. See LICENSE.txt for license information."
|
||||||
|
description: "This document includes a list of open source components used in Mattermost Desktop, including those that have been modified."
|
||||||
|
reviewers:
|
||||||
|
- mattermost/release-managers
|
||||||
|
- mattermost/team-desktop
|
||||||
|
search:
|
||||||
|
- "package.json"
|
||||||
|
dependencies:
|
||||||
|
- "wix"
|
||||||
|
devDependencies:
|
||||||
|
- "webpack"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Purpose |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| title | string | Field content will be used as a title of the application. See first line of `NOTICE.txt` file. |
|
||||||
|
| copyright | string | Field content will be used as a copyright message. See second line of `NOTICE.txt` file. |
|
||||||
|
| description | string | Field content will be used as notice file description. See third line of `NOTICE.txt` file. |
|
||||||
|
| reviewers | array of GitHub user/teams | Those will be automatically assigned to the PRs as reviewers. |
|
||||||
|
| dependencies | array | If any dependency name mentioned, it will be automatically added even if it is not a first-level dependency. |
|
||||||
|
| devDependencies | array | If any dependency name mentioned, it will be added when it is referenced in devDependency section. |
|
||||||
|
| search | array | Pipeline will search for package.json files mentioned here. Globstar format is supported ie. `packages/**/package.json`. |
|
||||||
|
|
14
.config/notice-file/config.yaml
Normal file
14
.config/notice-file/config.yaml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
title: "Mattermost Desktop"
|
||||||
|
copyright: "© 2016-2017 Mattermost, Inc. All Rights Reserved. See LICENSE.txt for license information."
|
||||||
|
description: "This document includes a list of open source components used in Mattermost Desktop, including those that have been modified."
|
||||||
|
reviewers:
|
||||||
|
- mattermost/release-managers
|
||||||
|
- mattermost/team-desktop
|
||||||
|
search:
|
||||||
|
- "package.json"
|
||||||
|
additionalDependencies:
|
||||||
|
- wix
|
||||||
|
includeDevDependencies: true
|
||||||
|
...
|
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*.{js|ts}]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
5
.eslintignore
Normal file
5
.eslintignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
api-types/lib
|
||||||
|
e2e/mochawesome-report
|
||||||
|
e2e/dist
|
174
.eslintrc.json
Normal file
174
.eslintrc.json
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"plugin:@mattermost/react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"formatjs",
|
||||||
|
"no-only-tests"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"webpack": {
|
||||||
|
"config": "webpack.config.base.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"header/header": [
|
||||||
|
2,
|
||||||
|
"line",
|
||||||
|
[
|
||||||
|
" Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.",
|
||||||
|
" See LICENSE.txt for license information."
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"import/no-commonjs": 2,
|
||||||
|
"no-process-env": 0,
|
||||||
|
"no-var": 2,
|
||||||
|
"react/no-find-dom-node": 2,
|
||||||
|
"no-underscore-dangle": ["warn"],
|
||||||
|
"@mattermost/use-external-link": 0,
|
||||||
|
"linebreak-style": 0,
|
||||||
|
"import/order": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"newlines-between": "always",
|
||||||
|
"groups": [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal",
|
||||||
|
"sibling",
|
||||||
|
"parent",
|
||||||
|
"index"
|
||||||
|
],
|
||||||
|
"pathGroups": [
|
||||||
|
{
|
||||||
|
"pattern": "@mattermost/**",
|
||||||
|
"group": "external",
|
||||||
|
"position": "after"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "@(app|common|main|renderer){,/**}",
|
||||||
|
"group": "internal",
|
||||||
|
"position": "before"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "types{,/**}",
|
||||||
|
"group": "internal",
|
||||||
|
"position": "after"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alphabetize": {
|
||||||
|
"order": "asc",
|
||||||
|
"caseInsensitive": true
|
||||||
|
},
|
||||||
|
"distinctGroup": true,
|
||||||
|
"pathGroupsExcludedImportTypes": ["builtin"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"scripts/**/*",
|
||||||
|
"src/main/preload/**/*",
|
||||||
|
"src/renderer/**/*"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-console": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"scripts/**/*",
|
||||||
|
"babel.config.js",
|
||||||
|
"webpack.config.*.js"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"import/no-commonjs": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"e2e/**/*",
|
||||||
|
"src/**/*.test.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-unused-expressions": 0, //TODO: rework tests to use correct notation
|
||||||
|
"func-names": 0,
|
||||||
|
"global-require": 0,
|
||||||
|
"new-cap": 0,
|
||||||
|
"prefer-arrow-callback": 0,
|
||||||
|
"no-import-assign": 0,
|
||||||
|
"no-only-tests/no-only-tests": 1,
|
||||||
|
"import/no-commonjs": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["e2e/**/*"],
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"webpack": {
|
||||||
|
"config": "webpack.config.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"webpack.config.renderer.js",
|
||||||
|
"e2e/specs/startup/app.test.js",
|
||||||
|
"e2e/specs/settings.test.js",
|
||||||
|
"e2e/modules/utils.js",
|
||||||
|
"e2e/modules/environment.js",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"webpack.config.base.js",
|
||||||
|
"./babel.config.js",
|
||||||
|
"README.md",
|
||||||
|
"scripts/check_build_config.js",
|
||||||
|
"LICENSE.txt",
|
||||||
|
"src/main/contextMenu.ts",
|
||||||
|
"src/main/badge.ts",
|
||||||
|
"src/common/config/index.ts",
|
||||||
|
"src/common/config/buildConfig.ts",
|
||||||
|
"src/common/config/pastDefaultPreferences.ts",
|
||||||
|
"src/common/config/upgradePreferences.ts",
|
||||||
|
"src/common/config/RegistryConfig.ts",
|
||||||
|
"src/common/config/defaultPreferences.ts",
|
||||||
|
"src/common/JsonFileManager.ts",
|
||||||
|
"src/main/certificateStore.ts",
|
||||||
|
"src/main/allowProtocolDialog.ts",
|
||||||
|
"src/main/AutoLauncher.ts",
|
||||||
|
"src/main/menus/tray.ts",
|
||||||
|
"src/main/CriticalErrorHandler.ts",
|
||||||
|
"src/main/utils.ts",
|
||||||
|
"src/main/menus/app.ts",
|
||||||
|
"src/renderer/components/RemoveServerModal.tsx",
|
||||||
|
"src/renderer/components/MainPage.tsx",
|
||||||
|
"src/renderer/components/AutoSaveIndicator.tsx",
|
||||||
|
"src/renderer/components/TabBar.tsx",
|
||||||
|
"src/renderer/components/DestructiveConfirmModal.tsx",
|
||||||
|
"src/renderer/components/ErrorView.tsx",
|
||||||
|
"src/renderer/components/SettingsPage.tsx",
|
||||||
|
"src/renderer/components/NewServerModal.tsx",
|
||||||
|
"src/renderer/settings.tsx",
|
||||||
|
"src/renderer/index.tsx"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"header/header": [
|
||||||
|
2,
|
||||||
|
"line",
|
||||||
|
[
|
||||||
|
" Copyright (c) 2015-2016 Yuya Ochiai",
|
||||||
|
" Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.",
|
||||||
|
" See LICENSE.txt for license information."
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
85
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
85
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
name: Bug report
|
||||||
|
description: Create a report about an issue you found in the Mattermost Desktop App
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: "Type/Bug"
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## STOP! Before you file a bug report
|
||||||
|
Check that the issue is truly a Desktop App issue and not an issue with our Web App. The Desktop App makes use of the Mattermost Web App to render Mattermost. This repository is for issues with the Desktop App wrapper.
|
||||||
|
You can do this by going to your web browser (preferably Chrome) and attempting to reproduce the issue there.
|
||||||
|
If it does, the issue should be reported to the [main Mattermost repository](https://github.com/mattermost/mattermost/issues).
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Checks before filing an issue
|
||||||
|
description: Please ensure you can confirm the following
|
||||||
|
options:
|
||||||
|
- label: This issue doesn't reproduce on web browsers (such as in Chrome). If it does, [issue reports go to the Mattermost Server repository](https://github.com/mattermost/mattermost-server/issues).
|
||||||
|
required: true
|
||||||
|
- label: I have checked the [issue tracker](https://github.com/mattermost/desktop/issues) and have not found an issue that matches the one I'm filing.
|
||||||
|
required: true
|
||||||
|
- label: "This issue is not a troubleshooting question. Troubleshooting questions go here: https://forum.mattermost.com/c/trouble-shoot/16."
|
||||||
|
required: true
|
||||||
|
- label: "This issue is not a feature request. You can request features and make product suggestions here: https://mattermost.com/suggestions/."
|
||||||
|
required: true
|
||||||
|
- label: This issue reproduces on the most recent [stable version](https://github.com/mattermost/desktop/releases/latest), or the most recent [prerelease version](https://github.com/mattermost/desktop/releases) of the Mattermost Desktop App.
|
||||||
|
required: true
|
||||||
|
- label: I have read the [contribution guidelines](https://github.com/mattermost/desktop/blob/master/CONTRIBUTING.md).
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Mattermost Desktop Version
|
||||||
|
description: |
|
||||||
|
What version of the Desktop App are you using? You can find it by going to [Help] > [Version Number].
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: |
|
||||||
|
What operating system does this issue occur on? Please include the distribution name (if necessary) and architecture.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- Windows 10 x64
|
||||||
|
- macOS Ventura 13.2 Apple Silicon
|
||||||
|
- Ubuntu Linux 22.04 LTS x64
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Mattermost Server Version
|
||||||
|
description: |
|
||||||
|
Which version of the Mattermost Server did this occur on?
|
||||||
|
You can find your Mattermost Server version by [Mattermost Menu] > [About Mattermost], where [Mattermost Menu] can be accessed by clicking on the grid in the top-left corner.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: |
|
||||||
|
Include a clear description of the steps taken to reproduce this issue.
|
||||||
|
It is also helpful to attach a screenshot or video if applicable.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: Include a clear description of what you expect to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Observed behavior
|
||||||
|
description: Include a clear description of what actually happens.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Log Output
|
||||||
|
description: Please include output from the log files. You can find the location of the log files by going to [Help] > [Show logs].
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: If you have anything else to add to the ticket, you may do so here.
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
blank_issues_enabled: false
|
72
.github/ISSUE_TEMPLATE/crash_report.yml
vendored
Normal file
72
.github/ISSUE_TEMPLATE/crash_report.yml
vendored
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
name: Crash report
|
||||||
|
description: Create a report about a crash you experienced while using the Mattermost Desktop App
|
||||||
|
title: "[Crash]: "
|
||||||
|
labels: "Type/Crash Report"
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Before you file a crash report
|
||||||
|
description: Please ensure you can confirm the following
|
||||||
|
options:
|
||||||
|
- label: I have checked the [issue tracker](https://github.com/mattermost/desktop/issues) and have not found an issue that matches the one I'm filing.
|
||||||
|
required: true
|
||||||
|
- label: This issue doesn't reproduce on web browsers (such as in Chrome). If it does, [issue reports go to the Mattermost Server repository](https://github.com/mattermost/mattermost-server/issues).
|
||||||
|
required: true
|
||||||
|
- label: I have read the [contribution guidelines](https://github.com/mattermost/desktop/blob/master/CONTRIBUTING.md).
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Mattermost Desktop Version
|
||||||
|
description: |
|
||||||
|
What version of the Desktop App are you using? You can find it by going to [Help] > [Version Number]. If you cannot access that, please check which version you downloaded, or ask your system administrator.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: |
|
||||||
|
What operating system does this issue occur on? Please include the distribution name (if necessary) and architecture.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- Windows 10 x64
|
||||||
|
- macOS Ventura 13.2 Apple Silicon
|
||||||
|
- Ubuntu Linux 22.04 LTS x64
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Mattermost Server Version
|
||||||
|
description: |
|
||||||
|
Which version of the Mattermost Server did this occur on?
|
||||||
|
You can find your Mattermost Server version by [Mattermost Menu] > [About Mattermost], where [Mattermost Menu] can be accessed by clicking on the grid in the top-left corner.
|
||||||
|
If you cannot access this, ask your system administrator.
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: What type of crash did you experience?
|
||||||
|
options:
|
||||||
|
- White screen (I can see the top bar, but I cannot see my Mattermost server)
|
||||||
|
- Uncaught exception (I saw a dialog pop up that said "The Mattermost app quit unexpectedly")
|
||||||
|
- System crash (The application quit unexpectedly with no warning, or the operating system reported a crash)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Crash report details
|
||||||
|
description: |
|
||||||
|
Please provide any information you can about the crash you experienced.
|
||||||
|
|
||||||
|
- White screen: If you experience a white screen, please first verify that the same behaviour doesn't reproduce on the browser and if it doesn't, you can go to [View] > [Developer Tools for Current Server], clicking on the Console tab, then right-clicking on the logs area and clicking [Save as]. Then you can copy and paste the contents of that file here.
|
||||||
|
- Uncaught exception: If you receive a dialog that says "The Mattermost app quit unexpectedly", click on the Show Details button. Copy the text that is shown and paste it there.
|
||||||
|
- System crash: For any other crashes, please provide the trace log or Event Viewer output or anything else that might be relevant here.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Log Output
|
||||||
|
description: Please include output from the log files if relevant. You can find the location of the log files by going to [Help] > [Show logs]. If you cannot access that, you can find them [here](https://docs.mattermost.com/install/troubleshooting.html#mattermost-desktop-app-logs).
|
||||||
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: If you have anything else to add to the ticket, you may do so here.
|
64
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
64
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<!-- Thank you for contributing a pull request! Here are a few tips to help you:
|
||||||
|
|
||||||
|
1. If this is your first contribution, make sure you've read the Contribution Checklist https://developers.mattermost.com/contribute/getting-started/contribution-checklist/
|
||||||
|
2. Read our blog post about "Submitting Great PRs" https://developers.mattermost.com/blog/2019-01-24-submitting-great-prs
|
||||||
|
3. Take a look at other repository specific documentation at https://developers.mattermost.com/contribute
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Summary
|
||||||
|
<!--
|
||||||
|
A brief description of what this pull request does.
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Ticket Link
|
||||||
|
<!--
|
||||||
|
If this pull request addresses a Help Wanted ticket, please link the relevant GitHub issue, e.g.
|
||||||
|
|
||||||
|
Fixes https://github.com/mattermost/desktop/issues/XXXXX
|
||||||
|
|
||||||
|
Otherwise, link the JIRA ticket.
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Checklist
|
||||||
|
<!--
|
||||||
|
Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.
|
||||||
|
-->
|
||||||
|
- [ ] Added or updated unit tests (required for all new features)
|
||||||
|
- [ ] Has UI changes
|
||||||
|
- [ ] read and understood our [Contributing Guidelines](https://github.com/mattermost/desktop/blob/master/CONTRIBUTING.md)
|
||||||
|
- [ ] completed [Mattermost Contributor Agreement](https://mattermost.com/contribute/)
|
||||||
|
- [ ] executed `npm run lint:js` for proper code formatting
|
||||||
|
- [ ] Run E2E tests by adding label `Run Desktop E2E Tests`
|
||||||
|
|
||||||
|
#### Device Information
|
||||||
|
This PR was tested on: <!-- Device name(s), OS version(s) -->
|
||||||
|
|
||||||
|
#### Screenshots
|
||||||
|
<!--
|
||||||
|
If the PR includes UI changes, include screenshots/GIFs.
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Release Note
|
||||||
|
<!--
|
||||||
|
Add a release note for each of the following conditions:
|
||||||
|
|
||||||
|
* New features and improvements, including behavioural changes, UI changes
|
||||||
|
* Bug fixes and fixes of previous known issues
|
||||||
|
* Deprecation warnings, breaking changes, or compatibility notes
|
||||||
|
|
||||||
|
If no release notes are required write NONE. Use past-tense. Newlines are stripped.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```release-note
|
||||||
|
Added a new config setting ServiceSettings.FooBar. Added a new column Foo to the Users table.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note
|
||||||
|
NONE
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
|
||||||
|
```release-note
|
||||||
|
|
||||||
|
```
|
11
.github/actions/patch-nightly-version/action.yaml
vendored
Normal file
11
.github/actions/patch-nightly-version/action.yaml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Copyright 2022 Mattermost, Inc.
|
||||||
|
name: "patch-version"
|
||||||
|
description: This action is used to patch package.json version with the nightly build
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: ci/generate-version
|
||||||
|
id: generate-version
|
||||||
|
shell: bash
|
||||||
|
run: go run ${GITHUB_ACTION_PATH}/patch-version.go . # https://github.com/orgs/community/discussions/25910
|
42
.github/actions/patch-nightly-version/patch-version.go
vendored
Normal file
42
.github/actions/patch-nightly-version/patch-version.go
vendored
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
packageFileName := fmt.Sprintf("%s/package.json", args[0])
|
||||||
|
packageJson, err := os.Open(packageFileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
packageBytes, err := ioutil.ReadAll(packageJson)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var packageInfo map[string]interface{}
|
||||||
|
json.Unmarshal(packageBytes, &packageInfo)
|
||||||
|
|
||||||
|
originalVersion := fmt.Sprintf("%s", packageInfo["version"])
|
||||||
|
nightlyVersion := fmt.Sprintf("%s-nightly.%s", strings.Split(originalVersion, "-")[0], time.Now().Format("20060102"))
|
||||||
|
packageInfo["version"] = nightlyVersion
|
||||||
|
|
||||||
|
newPackageJson := strings.Replace(string(packageBytes), originalVersion, nightlyVersion, 1)
|
||||||
|
err = ioutil.WriteFile(packageFileName, []byte(newPackageJson), 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
packageJson.Close()
|
||||||
|
fmt.Println("Update package.json with version:", nightlyVersion)
|
||||||
|
}
|
30
.github/actions/test/action.yaml
vendored
Normal file
30
.github/actions/test/action.yaml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Copyright 2022 Mattermost, Inc.
|
||||||
|
name: "test-ci"
|
||||||
|
description: This action used to run universal tests for all OS
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
shell:
|
||||||
|
description: The shell to run the test
|
||||||
|
required: true
|
||||||
|
default: bash
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: ci/run-eslint
|
||||||
|
run: npm run lint:js-quiet
|
||||||
|
shell: ${{ inputs.shell }}
|
||||||
|
- name: ci/run-check-types
|
||||||
|
run: npm run check-types
|
||||||
|
shell: ${{ inputs.shell }}
|
||||||
|
- name: ci/run-i18n-check
|
||||||
|
shell: ${{ inputs.shell }}
|
||||||
|
run: |
|
||||||
|
npm run i18n-extract -- --desktop-dir .
|
||||||
|
git --no-pager diff --exit-code i18n/en.json
|
||||||
|
- name: ci/run-unit-ci
|
||||||
|
shell: ${{ inputs.shell }}
|
||||||
|
env:
|
||||||
|
ELECTRON_DISABLE_SANDBOX: "1"
|
||||||
|
run: |
|
||||||
|
npm run test:unit
|
11
.github/codeql/codeql-config.yml
vendored
Normal file
11
.github/codeql/codeql-config.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
name: "CodeQL config"
|
||||||
|
|
||||||
|
query-filters:
|
||||||
|
- exclude:
|
||||||
|
problem.severity:
|
||||||
|
- warning
|
||||||
|
- recommendation
|
||||||
|
|
||||||
|
paths-ignore:
|
||||||
|
- e2e
|
||||||
|
- '**/*.test.*'
|
181
.github/workflows/build-for-pr.yml
vendored
Normal file
181
.github/workflows/build-for-pr.yml
vendored
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
name: build-for-pr
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
env:
|
||||||
|
TERM: xterm
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux-for-pr:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: ${{ github.event.label.name == 'Build Apps for PR' }}
|
||||||
|
steps:
|
||||||
|
- name: ci/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
- name: ci/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: ci/install-dependencies
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
wget -qO - https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_22.04/Release.key | sudo apt-key add -
|
||||||
|
wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.20.1/yq_linux_amd64 && chmod a+x /usr/local/bin/yq
|
||||||
|
sudo apt-get update || true && sudo apt-get install -y ca-certificates libxtst-dev libpng++-dev gcc-aarch64-linux-gnu g++-aarch64-linux-gnu jq icnsutils graphicsmagick tzdata
|
||||||
|
npm ci
|
||||||
|
- name: ci/build
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/linux
|
||||||
|
npm run package:linux-tar
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/linux
|
||||||
|
- name: ci/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-linux
|
||||||
|
path: ./build/linux
|
||||||
|
retention-days: 10 ## No need to keep CI builds more than 10 days
|
||||||
|
|
||||||
|
windows-install-deps:
|
||||||
|
runs-on: windows-2022
|
||||||
|
if: ${{ github.event.label.name == 'Build Apps for PR' }}
|
||||||
|
steps:
|
||||||
|
- name: ci/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
- name: ci/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: ci/cache-node-modules
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-build-node-modules-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-node-modules
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
- name: ci/install-dependencies
|
||||||
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: npm ci --openssl_fips=''
|
||||||
|
|
||||||
|
build-win-for-pr:
|
||||||
|
runs-on: windows-2022
|
||||||
|
if: ${{ github.event.label.name == 'Build Apps for PR' }}
|
||||||
|
needs:
|
||||||
|
- windows-install-deps
|
||||||
|
steps:
|
||||||
|
- name: ci/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
- name: ci/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: ci/cache-node-modules
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-build-node-modules-${{ hashFiles('package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-node-modules
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
- name: ci/install-node-gyp
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
choco install yq --version 4.15.1 -y
|
||||||
|
npm i -g node-gyp
|
||||||
|
node-gyp install
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers"
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers" --arch arm64
|
||||||
|
- name: ci/install-dependencies
|
||||||
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
npm ci --openssl_fips=''
|
||||||
|
- name: ci/build
|
||||||
|
env:
|
||||||
|
MM_WIN_INSTALLERS: 1
|
||||||
|
PFX_KEY: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_PFX_KEY }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_CSC_KEY_PASSWORD }}
|
||||||
|
PFX: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_PFX }}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_CSC_LINK }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/win
|
||||||
|
npm run package:windows
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/win
|
||||||
|
- name: ci/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-windows
|
||||||
|
path: ./build/win
|
||||||
|
retention-days: 10 ## No need to keep CI builds more than 10 days
|
||||||
|
|
||||||
|
build-mac-for-pr:
|
||||||
|
runs-on: macos-12
|
||||||
|
if: ${{ github.event.label.name == 'Build Apps for PR' }}
|
||||||
|
steps:
|
||||||
|
- name: ci/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
- name: ci/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: ci/install-dependencies
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
brew install yq
|
||||||
|
jq '.mac.target=["zip"]' electron-builder.json | jq '.mac.gatekeeperAssess=false' > /tmp/electron-builder.json && cp /tmp/electron-builder.json .
|
||||||
|
npm ci
|
||||||
|
- name: ci/build
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_APPLE_ID_PASS }}
|
||||||
|
CSC_FOR_PULL_REQUEST: true
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_CSC_KEY_PASSWORD }}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_CSC_LINK }}
|
||||||
|
MAC_PROFILE: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_DMG_PROFILE }}
|
||||||
|
run: |
|
||||||
|
echo $MAC_PROFILE | base64 -D > ./mac.provisionprofile
|
||||||
|
mkdir -p ./build/macos
|
||||||
|
npm run package:mac
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/macos/
|
||||||
|
- name: ci/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-macos
|
||||||
|
path: ./build/macos/
|
||||||
|
retention-days: 10 ## No need to keep CI builds more than 10 days
|
214
.github/workflows/ci.yaml
vendored
Normal file
214
.github/workflows/ci.yaml
vendored
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
name: ci
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
env:
|
||||||
|
TERM: xterm
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: ci/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
- name: ci/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: ci/install-dependencies
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
wget -qO - https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_22.04/Release.key | sudo apt-key add -
|
||||||
|
wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.20.1/yq_linux_amd64 && chmod a+x /usr/local/bin/yq
|
||||||
|
sudo apt-get update || true && sudo apt-get install -y ca-certificates libxtst-dev libpng++-dev gcc-aarch64-linux-gnu g++-aarch64-linux-gnu jq icnsutils graphicsmagick tzdata
|
||||||
|
npm ci
|
||||||
|
- name: ci/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: ci/build
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/linux
|
||||||
|
npm run package:linux-tar
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/linux
|
||||||
|
- name: ci/upload-test-results
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: linux-test-results
|
||||||
|
path: test-results.xml
|
||||||
|
retention-days: 5
|
||||||
|
- name: ci/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-linux
|
||||||
|
path: ./build/linux
|
||||||
|
retention-days: 10 ## No need to keep CI builds more than 10 days
|
||||||
|
|
||||||
|
windows-install-deps:
|
||||||
|
runs-on: windows-2022
|
||||||
|
steps:
|
||||||
|
- name: ci/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
- name: ci/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: ci/cache-node-modules
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-build-node-modules-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-node-modules
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
- name: ci/install-dependencies
|
||||||
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
choco install yq --version 4.15.1 -y
|
||||||
|
npm i -g node-gyp
|
||||||
|
node-gyp install
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers"
|
||||||
|
npm ci --openssl_fips=''
|
||||||
|
|
||||||
|
build-win-no-installer:
|
||||||
|
runs-on: windows-2022
|
||||||
|
needs:
|
||||||
|
- windows-install-deps
|
||||||
|
steps:
|
||||||
|
- name: ci/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
- name: ci/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: ci/cache-node-modules
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-build-node-modules-${{ hashFiles('package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-node-modules
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
- name: ci/install-node-gyp
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
choco install yq --version 4.15.1 -y
|
||||||
|
npm i -g node-gyp
|
||||||
|
node-gyp install
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers"
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers" --arch arm64
|
||||||
|
- name: ci/install-dependencies
|
||||||
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
npm ci --openssl_fips=''
|
||||||
|
- name: ci/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: ci/build
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/win
|
||||||
|
npm run package:windows
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/win
|
||||||
|
- name: ci/upload-test-results
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: windows-test-results
|
||||||
|
path: test-results.xml
|
||||||
|
retention-days: 5
|
||||||
|
- name: ci/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-windows
|
||||||
|
path: ./build/win
|
||||||
|
retention-days: 10 ## No need to keep CI builds more than 10 days
|
||||||
|
|
||||||
|
build-mac-no-dmg:
|
||||||
|
runs-on: macos-12
|
||||||
|
steps:
|
||||||
|
- name: ci/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
- name: ci/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: ci/install-dependencies
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
brew install yq
|
||||||
|
jq '.mac.target=["zip"]' electron-builder.json | jq '.mac.gatekeeperAssess=false' > /tmp/electron-builder.json && cp /tmp/electron-builder.json .
|
||||||
|
npm ci
|
||||||
|
- name: ci/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: ci/build
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/macos
|
||||||
|
npm run package:mac
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/macos/
|
||||||
|
- name: ci/upload-test-results
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: macos-test-results
|
||||||
|
path: test-results.xml
|
||||||
|
retention-days: 5
|
||||||
|
- name: ci/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-macos
|
||||||
|
path: ./build/macos/
|
||||||
|
retention-days: 10 ## No need to keep CI builds more than 10 days
|
||||||
|
|
||||||
|
report-test-results:
|
||||||
|
if: always()
|
||||||
|
needs:
|
||||||
|
- build-mac-no-dmg
|
||||||
|
- build-win-no-installer
|
||||||
|
- build-linux
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
checks: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: ci/download-macos-test-results
|
||||||
|
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||||
|
with:
|
||||||
|
name: macos-test-results
|
||||||
|
path: macos-test-results
|
||||||
|
- name: ci/download-windows-test-results
|
||||||
|
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||||
|
with:
|
||||||
|
name: windows-test-results
|
||||||
|
path: windows-test-results
|
||||||
|
- name: ci/download-linux-test-results
|
||||||
|
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||||
|
with:
|
||||||
|
name: linux-test-results
|
||||||
|
path: linux-test-results
|
||||||
|
- name: ci/publish-results
|
||||||
|
uses: EnricoMi/publish-unit-test-result-action@a3caf02865c0604ad3dc1ecfcc5cdec9c41b7936 # v2.3.0
|
||||||
|
with:
|
||||||
|
comment_mode: failures
|
||||||
|
compare_to_earlier_commit: false
|
||||||
|
junit_files: "**/*.xml"
|
40
.github/workflows/codeql-analysis.yml
vendored
Normal file
40
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [master]
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * 0"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: ["javascript"]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@423a04bb2cb7cd2643007122588f1387778f14d0 # v2.16.5
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
config-file: ./.github/codeql/codeql-config.yml
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@423a04bb2cb7cd2643007122588f1387778f14d0 # v2.16.5
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@423a04bb2cb7cd2643007122588f1387778f14d0 # v2.16.5
|
136
.github/workflows/compatibility-matrix-testing.yml
vendored
Normal file
136
.github/workflows/compatibility-matrix-testing.yml
vendored
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
name: Compatibility Matrix Testing
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
CMT_MATRIX:
|
||||||
|
description: "A JSON object representing the testing matrix"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
DESKTOP_VERSION:
|
||||||
|
description: "The desktop version to test"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
## This is picked up after the finish for cleanup
|
||||||
|
upload-cmt-server-detals:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: cmt/generate-instance-details-file
|
||||||
|
run: echo '${{ inputs.CMT_MATRIX }}' > instance-details.json
|
||||||
|
|
||||||
|
- name: cmt/upload-instance-details
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: instance-details.json
|
||||||
|
path: instance-details.json
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
calculate-commit-hash:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
DESKTOP_SHA: ${{ steps.repo.outputs.DESKTOP_SHA }}
|
||||||
|
steps:
|
||||||
|
- name: cmt/checkout-desktop
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.DESKTOP_VERSION }}
|
||||||
|
|
||||||
|
- name: cmt/calculate-mattermost-sha
|
||||||
|
id: repo
|
||||||
|
run: echo "DESKTOP_SHA=$(git rev-parse HEAD)" >> ${GITHUB_OUTPUT}
|
||||||
|
|
||||||
|
update-initial-status:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- calculate-commit-hash
|
||||||
|
steps:
|
||||||
|
- uses: mattermost/actions/delivery/update-commit-status@746563b58e737a17a8ceb00b84a813b9e6e1b236
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
repository_full_name: mattermost/desktop
|
||||||
|
commit_sha: ${{ needs.calculate-commit-hash.outputs.DESKTOP_SHA }}
|
||||||
|
context: e2e/compatibility-matrix-testing
|
||||||
|
description: "Compatibility Matrix Testing for ${{ inputs.DESKTOP_VERSION }} version"
|
||||||
|
status: pending
|
||||||
|
|
||||||
|
# Input follows the below schema
|
||||||
|
# {
|
||||||
|
# "environment": [
|
||||||
|
# {
|
||||||
|
# "os": "linux",
|
||||||
|
# "runner": "ubuntu-22.04"
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "os": "macos",
|
||||||
|
# "runner": "macos-13"
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "os": "windows",
|
||||||
|
# "runner": "windows-2022"
|
||||||
|
# }
|
||||||
|
# ],
|
||||||
|
# "server": [
|
||||||
|
# {
|
||||||
|
# "version": "9.6.1",
|
||||||
|
# "url": "https://delivery-cmt-8467830017-9-6-1.test.mattermost.cloud/"
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "version": "9.5.2",
|
||||||
|
# "url": "https://delivery-cmt-8467830017-9-5-2.test.mattermost.cloud/"
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
e2e:
|
||||||
|
name: ${{ matrix.environment.os }}-${{ matrix.server.version }}
|
||||||
|
uses: ./.github/workflows/e2e-functional-template.yml
|
||||||
|
needs:
|
||||||
|
- update-initial-status
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix: ${{ fromJson(inputs.CMT_MATRIX) }}
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
runs-on: ${{ matrix.environment.runner }}
|
||||||
|
cmt: true
|
||||||
|
MM_TEST_SERVER_URL: ${{ matrix.server.url }}
|
||||||
|
DESKTOP_VERSION: ${{ inputs.DESKTOP_VERSION }}
|
||||||
|
MM_SERVER_VERSION: ${{ matrix.server.version }}
|
||||||
|
|
||||||
|
# We need to duplicate here in order to set the proper commit status
|
||||||
|
# https://mattermost.atlassian.net/browse/CLD-5815
|
||||||
|
update-failure-final-status:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: failure() || cancelled()
|
||||||
|
needs:
|
||||||
|
- calculate-commit-hash
|
||||||
|
- e2e
|
||||||
|
steps:
|
||||||
|
- uses: mattermost/actions/delivery/update-commit-status@746563b58e737a17a8ceb00b84a813b9e6e1b236
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
repository_full_name: mattermost/desktop
|
||||||
|
commit_sha: ${{ needs.calculate-commit-hash.outputs.DESKTOP_SHA }}
|
||||||
|
context: e2e/compatibility-matrix-testing
|
||||||
|
description: "Compatibility Matrix Testing for ${{ inputs.DESKTOP_VERSION }} version"
|
||||||
|
status: failure
|
||||||
|
|
||||||
|
# https://mattermost.atlassian.net/browse/CLD-5815
|
||||||
|
update-success-final-status:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: success()
|
||||||
|
needs:
|
||||||
|
- calculate-commit-hash
|
||||||
|
- e2e
|
||||||
|
steps:
|
||||||
|
- uses: mattermost/actions/delivery/update-commit-status@746563b58e737a17a8ceb00b84a813b9e6e1b236
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
repository_full_name: mattermost/desktop
|
||||||
|
commit_sha: ${{ needs.calculate-commit-hash.outputs.DESKTOP_SHA }}
|
||||||
|
context: e2e/compatibility-matrix-testing
|
||||||
|
description: "Compatibility Matrix Testing for ${{ inputs.DESKTOP_VERSION }} version"
|
||||||
|
status: success
|
209
.github/workflows/e2e-functional-template.yml
vendored
Normal file
209
.github/workflows/e2e-functional-template.yml
vendored
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
name: E2E Functional Tests Template
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
MM_TEST_SERVER_URL:
|
||||||
|
description: "The test server URL"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
MM_TEST_USER_NAME:
|
||||||
|
description: "The admin username of the test instance"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
MM_TEST_PASSWORD:
|
||||||
|
description: "The admin password of the test instance"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
DESKTOP_VERSION:
|
||||||
|
description: "The desktop version to test"
|
||||||
|
required: false
|
||||||
|
default: ${{ github.ref }}
|
||||||
|
type: string
|
||||||
|
runs-on:
|
||||||
|
type: string
|
||||||
|
description: "The E2E tests underlying OS"
|
||||||
|
required: true
|
||||||
|
default: "ubuntu-22.04"
|
||||||
|
nightly:
|
||||||
|
type: boolean
|
||||||
|
description: "True if this is nigtly build"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
cmt:
|
||||||
|
type: boolean
|
||||||
|
description: "True if this is Compatibility Matrix Testing"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
MM_SERVER_VERSION:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "9.9.1"
|
||||||
|
outputs:
|
||||||
|
COMMENT_BODY:
|
||||||
|
description: "The output to comment"
|
||||||
|
value: ${{ jobs.e2e.outputs.COMMENT_BODY }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
AWS_S3_BUCKET: "mattermost-cypress-report"
|
||||||
|
BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
|
BUILD_TAG: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
JIRA_PROJECT_KEY: "MM"
|
||||||
|
MM_TEST_SERVER_URL: ${{ inputs.MM_TEST_SERVER_URL || secrets.MM_DESKTOP_E2E_SERVER_URL }}
|
||||||
|
MM_TEST_USER_NAME: ${{ inputs.MM_TEST_USER_NAME || secrets.MM_DESKTOP_E2E_USER_NAME }}
|
||||||
|
MM_TEST_PASSWORD: ${{ inputs.MM_TEST_PASSWORD || secrets.MM_DESKTOP_E2E_USER_CREDENTIALS }}
|
||||||
|
PULL_REQUEST: "https://github.com/mattermost/desktop/pull/${{ github.event.number }}"
|
||||||
|
ZEPHYR_ENVIRONMENT_NAME: "Desktop app"
|
||||||
|
ZEPHYR_FOLDER_ID: "12413253"
|
||||||
|
TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_DESKTOP_E2E_TEST_CYCLE_LINK_PREFIX }}
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.MM_DESKTOP_E2E_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_DESKTOP_E2E_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_REGION: "us-east-1"
|
||||||
|
WEBHOOK_URL: ${{ secrets.MM_DESKTOP_E2E_WEBHOOK_URL }}
|
||||||
|
ZEPHYR_API_KEY: ${{ secrets.MM_DESKTOP_E2E_ZEPHYR_API_KEY }}
|
||||||
|
REPORT_LINK: "none"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
runs-on: ${{ inputs.runs-on }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
outputs:
|
||||||
|
COMMENT_BODY: ${{ steps.analyze-flaky-tests.outputs.COMMENT_BODY }}
|
||||||
|
steps:
|
||||||
|
- name: e2e/set-required-variables
|
||||||
|
id: variables
|
||||||
|
run: |
|
||||||
|
RUNNER_OS=$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" == "pull_request" ]; then
|
||||||
|
echo "BUILD_SUFFIX=desktop-pr-${RUNNER_OS}" >> $GITHUB_OUTPUT
|
||||||
|
echo "TYPE=PR" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
elif [ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.cmt }}" ]; then
|
||||||
|
echo "BUILD_SUFFIX=desktop-release-${RUNNER_OS}" >> $GITHUB_OUTPUT
|
||||||
|
echo "TYPE=CMT" >> $GITHUB_ENV
|
||||||
|
echo "ZEPHYR_ENABLE=true" >> $GITHUB_ENV
|
||||||
|
echo "ZEPHYR_FOLDER_LINUX_REPORT=12358649" >> $GITHUB_ENV
|
||||||
|
echo "ZEPHYR_FOLDER_MACOS_REPORT=12358650" >> $GITHUB_ENV
|
||||||
|
echo "ZEPHYR_FOLDER_WIN_REPORT=12358651" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
elif [ "${{ github.event_name }}" == "workflow_dispatch" ] && ! ${{ inputs.nightly }}; then
|
||||||
|
echo "BUILD_SUFFIX=desktop-manual-trigger-${RUNNER_OS}" >> $GITHUB_OUTPUT
|
||||||
|
echo "TYPE=MANUAL" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/master" ]; then
|
||||||
|
echo "BUILD_SUFFIX=desktop-master-push-${RUNNER_OS}" >> $GITHUB_OUTPUT
|
||||||
|
echo "TYPE=MASTER" >> $GITHUB_ENV
|
||||||
|
echo "ZEPHYR_ENABLE=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
elif ${{ inputs.nightly }}; then
|
||||||
|
echo "BUILD_SUFFIX=desktop-nightly-${RUNNER_OS}" >> $GITHUB_OUTPUT
|
||||||
|
echo "TYPE=NIGHTLY" >> $GITHUB_ENV
|
||||||
|
echo "ZEPHYR_ENABLE=true" >> $GITHUB_ENV
|
||||||
|
echo "ZEPHYR_FOLDER_LINUX_REPORT=12363689" >> $GITHUB_ENV
|
||||||
|
echo "ZEPHYR_FOLDER_MACOS_REPORT=12363687" >> $GITHUB_ENV
|
||||||
|
echo "ZEPHYR_FOLDER_WIN_REPORT=12363690" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: e2e/set-build-id
|
||||||
|
run: echo "BUILD_ID=${{ github.run_id }}-${{ steps.variables.outputs.BUILD_SUFFIX }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: e2e/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.DESKTOP_VERSION }}
|
||||||
|
|
||||||
|
- name: e2e/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
|
||||||
|
- name: e2e/cache-node-modules
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
node_modules
|
||||||
|
C:\Users\runneradmin\.electron-gyp
|
||||||
|
key: ${{ runner.os }}-build-node-modules-${{ hashFiles('./package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-node-modules
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: e2e/setup-python
|
||||||
|
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
|
||||||
|
## Linux Depdendencies
|
||||||
|
- name: e2e/install-dependencies-linux
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 0
|
||||||
|
run: |
|
||||||
|
wget -qO - https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_22.04/Release.key | sudo apt-key add -
|
||||||
|
wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.20.1/yq_linux_amd64 && chmod a+x /usr/local/bin/yq
|
||||||
|
sudo apt-get update || true && sudo apt-get install -y ca-certificates libxtst-dev libpng++-dev gcc-aarch64-linux-gnu g++-aarch64-linux-gnu jq icnsutils graphicsmagick tzdata xsel
|
||||||
|
npm ci
|
||||||
|
cd e2e
|
||||||
|
npm ci
|
||||||
|
npx electron-rebuild --platform=linux -f -t prod,optional,dev -w robotjs --module-dir ../
|
||||||
|
|
||||||
|
- name: e2e/install-dependencies-macos
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
jq '.mac.target=["zip"]' electron-builder.json | jq '.mac.gatekeeperAssess=false' > /tmp/electron-builder.json && cp /tmp/electron-builder.json .
|
||||||
|
npm ci
|
||||||
|
cd e2e
|
||||||
|
npm ci
|
||||||
|
npx electron-rebuild --platform=darwin -f -t prod,optional,dev -w robotjs --module-dir ../
|
||||||
|
|
||||||
|
## Windows Dependencies
|
||||||
|
- name: e2e/install-dependencies-windows
|
||||||
|
if: steps.cache-node-modules.outputs.cache-hit != 'true' && runner.os == 'Windows'
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
choco install yq --version 4.15.1 -y
|
||||||
|
npm i -g node-gyp
|
||||||
|
node-gyp install
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers"
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers" --arch arm64
|
||||||
|
npm ci --openssl_fips=''
|
||||||
|
cd e2e
|
||||||
|
npm ci
|
||||||
|
npx electron-rebuild --platform=win32 -f -t prod,optional,dev -w robotjs --module-dir ../
|
||||||
|
|
||||||
|
- name: e2e/run-playright-tests-${{ runner.os }}
|
||||||
|
run: |
|
||||||
|
if [ ${{ runner.os }} == 'Linux' ]; then
|
||||||
|
export DISPLAY=:99
|
||||||
|
Xvfb $DISPLAY -screen 0 1280x960x24 > /dev/null 2>&1 &
|
||||||
|
fi
|
||||||
|
npm run build-test
|
||||||
|
cd e2e
|
||||||
|
npm run run:e2e || true # making job pass even if the tests fail due to flakyness
|
||||||
|
npm run send-report
|
||||||
|
env:
|
||||||
|
SERVER_VERSION: ${{ inputs.MM_SERVER_VERSION}}
|
||||||
|
DESKTOP_VERSION: ${{ inputs.DESKTOP_VERSION }}
|
||||||
|
|
||||||
|
- name: e2e/analyze-flaky-tests
|
||||||
|
id: analyze-flaky-tests
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
process.chdir('./e2e');
|
||||||
|
const { analyzeFlakyTests } = require('./utils/analyze-flaky-test.js');
|
||||||
|
const { commentBody, newFailedTests } = analyzeFlakyTests();
|
||||||
|
core.setOutput('COMMENT_BODY', commentBody);
|
||||||
|
if (newFailedTests.length > 0) {
|
||||||
|
core.setFailed('E2E tests failed.');
|
||||||
|
}
|
129
.github/workflows/e2e-functional.yml
vendored
Normal file
129
.github/workflows/e2e-functional.yml
vendored
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
name: Electron Playwright Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version_name:
|
||||||
|
type: string
|
||||||
|
description: "Desktop Version name eg: 5.6"
|
||||||
|
required: true
|
||||||
|
job_name:
|
||||||
|
type: choice
|
||||||
|
description: "Job name"
|
||||||
|
required: true
|
||||||
|
default: "All"
|
||||||
|
options:
|
||||||
|
- "e2e-linux"
|
||||||
|
- "e2e-macos"
|
||||||
|
- "e2e-windows"
|
||||||
|
- "All"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e-linux:
|
||||||
|
if: ${{
|
||||||
|
(
|
||||||
|
(inputs.job_name == 'e2e-linux' || inputs.job_name == 'All')
|
||||||
|
&&
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
github.event_name == 'push'
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
github.event_name == 'pull_request' &&
|
||||||
|
github.event.pull_request.labels &&
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'Run Desktop E2E Tests')
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
uses: ./.github/workflows/e2e-functional-template.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
DESKTOP_VERSION: ${{ inputs.version_name || github.head_ref || github.ref }}
|
||||||
|
|
||||||
|
e2e-macos:
|
||||||
|
if: ${{
|
||||||
|
(
|
||||||
|
(inputs.job_name == 'e2e-macos' || inputs.job_name == 'All')
|
||||||
|
&&
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
github.event_name == 'push'
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
github.event_name == 'pull_request' &&
|
||||||
|
github.event.pull_request.labels &&
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'Run Desktop E2E Tests')
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
uses: ./.github/workflows/e2e-functional-template.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
runs-on: macos-13
|
||||||
|
DESKTOP_VERSION: ${{ inputs.version_name || github.head_ref || github.ref }}
|
||||||
|
|
||||||
|
e2e-windows:
|
||||||
|
if: ${{
|
||||||
|
(
|
||||||
|
(inputs.job_name == 'e2e-windows' || inputs.job_name == 'All')
|
||||||
|
&&
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
github.event_name == 'push'
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
github.event_name == 'pull_request' &&
|
||||||
|
github.event.pull_request.labels &&
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'Run Desktop E2E Tests')
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
uses: ./.github/workflows/e2e-functional-template.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
runs-on: windows-2022
|
||||||
|
DESKTOP_VERSION: ${{ inputs.version_name || github.head_ref || github.ref }}
|
||||||
|
|
||||||
|
e2e-remove-label:
|
||||||
|
if: ${{ always() && contains(github.event.pull_request.labels.*.name, 'Run Desktop E2E Tests') }}
|
||||||
|
needs: [e2e-linux, e2e-macos, e2e-windows]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: e2e/unify-comments-in-single-comment
|
||||||
|
run: |
|
||||||
|
echo "PR_COMMENT<<EOF" >> "${GITHUB_ENV}"
|
||||||
|
echo "Here are the test results below:" >> "${GITHUB_ENV}"
|
||||||
|
echo "${{ needs.e2e-linux.outputs.COMMENT_BODY }}" >> "${GITHUB_ENV}"
|
||||||
|
echo "${{ needs.e2e-macos.outputs.COMMENT_BODY }}" >> "${GITHUB_ENV}"
|
||||||
|
echo "${{ needs.e2e-windows.outputs.COMMENT_BODY }}" >> "${GITHUB_ENV}"
|
||||||
|
echo "EOF" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
|
- name: e2e/send-comment-results-in-pr
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: process.env.PR_COMMENT,
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: e2e/remove-label-from-pr
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
continue-on-error: true # Label might have been removed manually
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.removeLabel({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
name: 'Run Desktop E2E Tests',
|
||||||
|
});
|
96
.github/workflows/e2e-performance.yml
vendored
Normal file
96
.github/workflows/e2e-performance.yml
vendored
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
name: E2E Performance Tests (Desktop)
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
|
||||||
|
env:
|
||||||
|
RESULTS_PATH: e2e/performance/perf-test-report.json
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: ${{ github.event.label.name == 'Run E2E Performance Tests' }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
node-version: [16]
|
||||||
|
steps:
|
||||||
|
- name: Add start comment
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: `E2E Performance Tests started 🏎️`,
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: Set env variable for timestamp
|
||||||
|
run: echo "NOW=$(date +'%Y-%m-%dT%H:%M:%S')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
run: sudo apt-get install libxtst-dev libpng++-dev
|
||||||
|
|
||||||
|
- name: Install dependencies 👨🏻💻
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: E2E Performance Tests for Electron 🧪
|
||||||
|
run: ELECTRON_DISABLE_SANDBOX=1 xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- cd e2e && npm run test:performance
|
||||||
|
|
||||||
|
- name: Upload artifact to Github
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: perf-test-report.json
|
||||||
|
path: ${{ env.RESULTS_PATH }}
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_PERFORMANCE_TESTS_PUT_BUCKET }}
|
||||||
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PERFORMANCE_TESTS_PUT_BUCKET }}
|
||||||
|
aws-region: ${{ secrets.AWS_REGION_PERFORMANCE_TESTS_PUT_BUCKET }}
|
||||||
|
mask-aws-account-id: true
|
||||||
|
|
||||||
|
- name: Upload report to S3
|
||||||
|
run: aws s3 cp ${{ env.RESULTS_PATH }} s3://${{ secrets.AWS_BUCKET_PERFORMANCE_TESTS }}/${{ github.head_ref }}-${{ github.sha }}-${{ env.NOW }}.json
|
||||||
|
|
||||||
|
- name: Add results in PR comment
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const {generateCommentBodyPerformanceTest} = require('./e2e/utils/pr-e2e-durations-report.js');
|
||||||
|
const fileContents = fs.readFileSync('${{ env.RESULTS_PATH }}');
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: generateCommentBodyPerformanceTest(fileContents),
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: Remove "Run E2E Performance Tests" label
|
||||||
|
if: always()
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
continue-on-error: true # Label might have been removed manually
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.removeLabel({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
name: 'Run E2E Performance Tests',
|
||||||
|
});
|
60
.github/workflows/nightly-builds.yaml
vendored
Normal file
60
.github/workflows/nightly-builds.yaml
vendored
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
name: nightly-builds
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: 0 4 * * 0-5
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tag-nightly-build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
tag: ${{ steps.tag-creation.outputs.tag }}
|
||||||
|
steps:
|
||||||
|
- name: nightly/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
- name: nightly/patch-version
|
||||||
|
uses: ./.github/actions/patch-nightly-version
|
||||||
|
- name: nightly/create-nightly-build-tag
|
||||||
|
id: tag-creation
|
||||||
|
run: |
|
||||||
|
git config --global user.email "nightly-build@mattermost.com"
|
||||||
|
git config --global user.name "Nightly Build"
|
||||||
|
git checkout -b "$(jq -r .version package.json)"
|
||||||
|
git add package.json
|
||||||
|
git commit -m "Nightly build $(jq -r .version package.json)"
|
||||||
|
git tag "$(jq -r .version package.json)" -m "Nightly build $(jq -r .version package.json)"
|
||||||
|
git push --tags --force
|
||||||
|
echo "tag=$(jq -r .version package.json)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
nightly-main:
|
||||||
|
needs:
|
||||||
|
- tag-nightly-build
|
||||||
|
uses: ./.github/workflows/nightly-main.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
tag: ${{ needs.tag-nightly-build.outputs.tag }}
|
||||||
|
|
||||||
|
nightly-rainforest:
|
||||||
|
needs:
|
||||||
|
- tag-nightly-build
|
||||||
|
uses: ./.github/workflows/nightly-rainforest.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
tag: ${{ needs.tag-nightly-build.outputs.tag }}
|
||||||
|
|
||||||
|
nightly-e2e:
|
||||||
|
needs:
|
||||||
|
- tag-nightly-build
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runs-on:
|
||||||
|
- macos-13
|
||||||
|
- ubuntu-22.04
|
||||||
|
- windows-2022
|
||||||
|
uses: ./.github/workflows/e2e-functional-template.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
|
nightly: true
|
||||||
|
DESKTOP_VERSION: ${{ needs.tag-nightly-build.outputs.tag }}
|
251
.github/workflows/nightly-main.yml
vendored
Normal file
251
.github/workflows/nightly-main.yml
vendored
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
name: nightly-main
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Reference tag of the nightly build"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Reference tag of the nightly build"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
env:
|
||||||
|
TERM: xterm
|
||||||
|
MM_WIN_INSTALLERS: 1
|
||||||
|
REFERENCE: ${{ inputs.tag }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: ci/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.REFERENCE }}
|
||||||
|
- name: ci/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: ci/install-dependencies
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
wget -qO - https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_22.04/Release.key | sudo apt-key add -
|
||||||
|
wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.20.1/yq_linux_amd64 && chmod a+x /usr/local/bin/yq
|
||||||
|
sudo apt-get update || true && sudo apt-get install -y ca-certificates libxtst-dev libpng++-dev gcc-aarch64-linux-gnu g++-aarch64-linux-gnu jq icnsutils graphicsmagick tzdata
|
||||||
|
npm ci
|
||||||
|
- name: ci/build
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/linux
|
||||||
|
npm run package:linux
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/linux
|
||||||
|
- name: ci/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-nightly-main-${{ runner.os }}
|
||||||
|
path: ./build
|
||||||
|
compression-level: 0
|
||||||
|
retention-days: 5 ## No need to keep them since they are uploaded on S3
|
||||||
|
|
||||||
|
build-msi-installer:
|
||||||
|
runs-on: windows-2022
|
||||||
|
steps:
|
||||||
|
- name: nightly/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.REFERENCE }}
|
||||||
|
- name: nightly/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: nightly/install-deps
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
choco install yq --version 4.15.1 -y
|
||||||
|
npm i -g node-gyp
|
||||||
|
node-gyp install
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers"
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers" --arch arm64
|
||||||
|
npm ci --openssl_fips=''
|
||||||
|
- name: nightly/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: nightly/build
|
||||||
|
shell: powershell
|
||||||
|
env:
|
||||||
|
MM_WIN_INSTALLERS: 1
|
||||||
|
PFX_KEY: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_PFX_KEY }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_CSC_KEY_PASSWORD }}
|
||||||
|
PFX: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_PFX }}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_CSC_LINK }}
|
||||||
|
run: npm run package:windows-installers
|
||||||
|
- name: nightly/package
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/win-release
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/win-release
|
||||||
|
- name: nightly/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-nightly-main-${{ runner.os }}
|
||||||
|
path: ./build
|
||||||
|
compression-level: 0
|
||||||
|
retention-days: 5 ## No need to keep them since they are uploaded on S3
|
||||||
|
|
||||||
|
mac-app-store-preflight:
|
||||||
|
runs-on: macos-12
|
||||||
|
env:
|
||||||
|
MAS_PROFILE: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_MAS_PROFILE }}
|
||||||
|
MACOS_API_KEY_ID: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_MACOS_API_KEY_ID }}
|
||||||
|
MACOS_API_KEY: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_MACOS_API_KEY }}
|
||||||
|
MACOS_API_ISSUER_ID: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_MACOS_API_ISSUER_ID }}
|
||||||
|
CSC_FOR_PULL_REQUEST: true
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_CSC_KEY_PASSWORD}}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_CSC_LINK }}
|
||||||
|
steps:
|
||||||
|
- name: nightly/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.REFERENCE }}
|
||||||
|
- name: nightly/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: nightly/install-dependencies
|
||||||
|
run: |
|
||||||
|
brew install yq
|
||||||
|
npm ci
|
||||||
|
- name: nightly/copy-provisioning-profile
|
||||||
|
run: echo $MAS_PROFILE | base64 -D > ./mas.provisionprofile
|
||||||
|
- name: nightly/patch-version-number-for-MAS
|
||||||
|
run: ./scripts/patch_mas_version.sh
|
||||||
|
- name: nightly/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: nightly/package
|
||||||
|
run: npm run package:mas
|
||||||
|
- name: nightly/publish
|
||||||
|
run: fastlane publish_test path:"$(find . -name \*.pkg -print -quit)"
|
||||||
|
|
||||||
|
build-mac-installer:
|
||||||
|
runs-on: macos-12
|
||||||
|
needs:
|
||||||
|
- mac-app-store-preflight
|
||||||
|
steps:
|
||||||
|
- name: nightly/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.REFERENCE }}
|
||||||
|
- name: nightly/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: nightly/install-dependencies
|
||||||
|
run: |
|
||||||
|
brew install yq rename
|
||||||
|
npm ci
|
||||||
|
- name: nightly/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: nightly/build
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_APPLE_ID_PASS }}
|
||||||
|
CSC_FOR_PULL_REQUEST: true
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_CSC_KEY_PASSWORD }}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_CSC_LINK }}
|
||||||
|
MAC_PROFILE: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_DMG_PROFILE }}
|
||||||
|
run: |
|
||||||
|
echo $MAC_PROFILE | base64 -D > ./mac.provisionprofile
|
||||||
|
mkdir -p ./build/macos-release
|
||||||
|
npm run package:mac-with-universal
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/macos-release
|
||||||
|
- name: nightly/rename-arm64-to-m1
|
||||||
|
run: rename 's/arm64/m1/' ./build/macos-release/$(jq -r .version package.json)/*
|
||||||
|
- name: nightly/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-nightly-main-${{ runner.os }}
|
||||||
|
path: ./build
|
||||||
|
compression-level: 0
|
||||||
|
retention-days: 5 ## No need to keep them since they are uploaded on S3
|
||||||
|
|
||||||
|
upload-to-s3:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
links: ${{ steps.generate-linklist.outputs.linklist }}
|
||||||
|
needs:
|
||||||
|
- build-mac-installer
|
||||||
|
- build-msi-installer
|
||||||
|
- build-linux
|
||||||
|
steps:
|
||||||
|
- name: nightly/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.REFERENCE }}
|
||||||
|
- name: nightly/setup-aws-credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # v1.7.0
|
||||||
|
with:
|
||||||
|
aws-region: us-east-1
|
||||||
|
aws-access-key-id: ${{ secrets.MM_DESKTOP_RELEASE_AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.MM_DESKTOP_RELEASE_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
- name: nightly/download-builds
|
||||||
|
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||||
|
with:
|
||||||
|
path: build
|
||||||
|
pattern: build-nightly-main-*
|
||||||
|
merge-multiple: true
|
||||||
|
- name: nightly/setup-files-for-aws
|
||||||
|
run: |
|
||||||
|
mkdir -p ./aws-s3-dist
|
||||||
|
cp -r --backup=numbered ./build/{macos-release,win-release,linux}/* ./aws-s3-dist
|
||||||
|
- name: nightly/upload-to-s3
|
||||||
|
run: aws s3 cp ./aws-s3-dist/ s3://releases.mattermost.com/desktop/ --acl public-read --cache-control "no-cache" --recursive
|
||||||
|
- name: nightly/generate-linklist
|
||||||
|
id: generate-linklist
|
||||||
|
run: |
|
||||||
|
mkdir -p ./links
|
||||||
|
echo "### Nightly builds:" > ./links/linklist.txt
|
||||||
|
echo "Links for $(date +"%b-%d-%Y")" >> ./links/linklist.txt
|
||||||
|
echo "##### :tux: Linux" >> ./links/linklist.txt
|
||||||
|
for i in `ls ./build/linux/$(jq -r .version package.json)/` ; do echo "- [$i](https://s3.amazonaws.com/releases.mattermost.com/desktop/$(jq -r .version package.json)/$i)" ; done >> ./links/linklist.txt
|
||||||
|
echo "##### :apple_logo: macOS" >> ./links/linklist.txt
|
||||||
|
for i in `ls ./build/macos-release/$(jq -r .version package.json)/` ; do echo "- [$i](https://s3.amazonaws.com/releases.mattermost.com/desktop/$(jq -r .version package.json)/$i)" ; done >> ./links/linklist.txt
|
||||||
|
echo "##### :windows: Windows" >> ./links/linklist.txt
|
||||||
|
for i in `ls ./build/win-release/$(jq -r .version package.json)/` ; do echo "- [$i](https://s3.amazonaws.com/releases.mattermost.com/desktop/$(jq -r .version package.json)/$i)" ; done >> ./links/linklist.txt
|
||||||
|
cat ./links/linklist.txt
|
||||||
|
LINKLIST=$(<./links/linklist.txt)
|
||||||
|
## https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||||
|
echo 'linklist<<EOF' >> $GITHUB_OUTPUT
|
||||||
|
echo "$LINKLIST" >> $GITHUB_OUTPUT
|
||||||
|
echo 'EOF' >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
share-links-to-channel:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- upload-to-s3
|
||||||
|
steps:
|
||||||
|
- name: nightly/share-links-to-channel
|
||||||
|
run: |
|
||||||
|
jq --null-input \
|
||||||
|
--arg icon_url "https://upload.wikimedia.org/wikipedia/commons/1/17/Luna_symbol.png" \
|
||||||
|
--arg username "NightBuilder" \
|
||||||
|
--arg text "${{ needs.upload-to-s3.outputs.links }}" \
|
||||||
|
'{"username":$username,"icon_url": $icon_url, "text": $text }' > /tmp/webhook-data.json
|
||||||
|
curl -i -X POST -H "Content-Type: application/json" -d @/tmp/webhook-data.json ${{ secrets.MM_DESKTOP_NIGHTLY_WEBHOOK_URL }}
|
149
.github/workflows/nightly-rainforest.yml
vendored
Normal file
149
.github/workflows/nightly-rainforest.yml
vendored
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
name: nightly-rainforest
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Reference tag of the nightly build"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Reference tag of the nightly build"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
env:
|
||||||
|
TERM: xterm
|
||||||
|
MM_DESKTOP_BUILD_DISABLEGPU: true
|
||||||
|
MM_DESKTOP_BUILD_SKIPONBOARDINGSCREENS: true
|
||||||
|
MM_WIN_INSTALLERS: 1
|
||||||
|
REFERENCE: ${{ inputs.tag }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-msi-installer:
|
||||||
|
runs-on: windows-2022
|
||||||
|
steps:
|
||||||
|
- name: nightly/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.REFERENCE }}
|
||||||
|
- name: nightly/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: nightly/install-deps
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
choco install yq --version 4.15.1 -y
|
||||||
|
npm i -g node-gyp
|
||||||
|
node-gyp install
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers"
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers" --arch arm64
|
||||||
|
npm ci --openssl_fips=''
|
||||||
|
- name: nightly/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: nightly/build
|
||||||
|
shell: powershell
|
||||||
|
env:
|
||||||
|
MM_WIN_INSTALLERS: 1
|
||||||
|
PFX_KEY: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_PFX_KEY }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_CSC_KEY_PASSWORD }}
|
||||||
|
PFX: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_PFX }}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_CSC_LINK }}
|
||||||
|
run: npm run package:windows-installers
|
||||||
|
- name: nightly/package
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/win
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/win
|
||||||
|
- name: nightly/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-rainforest-${{ runner.os }}
|
||||||
|
path: ./build
|
||||||
|
compression-level: 0
|
||||||
|
retention-days: 5 ## No need to keep them since they are uploaded on S3
|
||||||
|
|
||||||
|
build-mac-installer:
|
||||||
|
runs-on: macos-12
|
||||||
|
steps:
|
||||||
|
- name: nightly/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.REFERENCE }}
|
||||||
|
- name: nightly/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: nightly/install-dependencies
|
||||||
|
run: |
|
||||||
|
brew install yq rename
|
||||||
|
npm ci
|
||||||
|
- name: nightly/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: nightly/build
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_APPLE_ID_PASS }}
|
||||||
|
CSC_FOR_PULL_REQUEST: true
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_CSC_KEY_PASSWORD }}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_CSC_LINK }}
|
||||||
|
MAC_PROFILE: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_DMG_PROFILE }}
|
||||||
|
run: |
|
||||||
|
echo $MAC_PROFILE | base64 -D > ./mac.provisionprofile
|
||||||
|
mkdir -p ./build/macos
|
||||||
|
npm run package:mac-with-universal
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/macos
|
||||||
|
- name: nightly/rename-arm64-to-m1
|
||||||
|
run: rename 's/arm64/m1/' ./build/macos/$(jq -r .version package.json)/*
|
||||||
|
- name: nightly/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-rainforest-${{ runner.os }}
|
||||||
|
path: ./build
|
||||||
|
compression-level: 0
|
||||||
|
retention-days: 5 ## No need to keep them since they are uploaded on S3
|
||||||
|
|
||||||
|
upload-to-s3-daily:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- build-mac-installer
|
||||||
|
- build-msi-installer
|
||||||
|
steps:
|
||||||
|
- name: nightly/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.REFERENCE }}
|
||||||
|
- name: nightly/setup-aws-credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # v1.7.0
|
||||||
|
with:
|
||||||
|
aws-region: us-east-1
|
||||||
|
aws-access-key-id: ${{ secrets.MM_DESKTOP_DAILY_AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.MM_DESKTOP_DAILY_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
- name: nightly/download-builds
|
||||||
|
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||||
|
with:
|
||||||
|
path: build
|
||||||
|
pattern: build-rainforest-*
|
||||||
|
merge-multiple: true
|
||||||
|
- name: nightly/install-missing-deps
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install rename jq -y
|
||||||
|
- name: nightly/setup-files-for-aws
|
||||||
|
run: |
|
||||||
|
rename 's/\d+\.\d+\.\d+\-nightly\.\d+\/mattermost(.+)\d+\.\d+\.\d+\-nightly\.\d+/mattermost$1daily-develop/' ./build/macos/$(jq -r .version package.json)/*
|
||||||
|
rename 's/\d+\.\d+\.\d+\-nightly\.\d+\/mattermost(.+)\d+\.\d+\.\d+\-nightly\.\d+/mattermost$1daily-develop/' ./build/win/$(jq -r .version package.json)/*
|
||||||
|
- name: nightly/upload-to-s3
|
||||||
|
run: aws s3 cp ./build/ s3://mattermost-desktop-daily-builds/ --acl public-read --cache-control "no-cache" --recursive
|
49
.github/workflows/release-mas.yaml
vendored
Normal file
49
.github/workflows/release-mas.yaml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
name: release-mas
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
|
||||||
|
- "v[0-9]+.[0-9]+.[0-9]+-mas.[0-9]+"
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
env:
|
||||||
|
TERM: xterm
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mac-app-store-preflight:
|
||||||
|
runs-on: macos-12
|
||||||
|
env:
|
||||||
|
MAS_PROFILE: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_MAS_PROFILE }}
|
||||||
|
MACOS_API_KEY_ID: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_MACOS_API_KEY_ID }}
|
||||||
|
MACOS_API_KEY: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_MACOS_API_KEY }}
|
||||||
|
MACOS_API_ISSUER_ID: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_MACOS_API_ISSUER_ID }}
|
||||||
|
CSC_FOR_PULL_REQUEST: true
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_CSC_KEY_PASSWORD}}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MAC_APP_STORE_CSC_LINK }}
|
||||||
|
steps:
|
||||||
|
- name: release/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
- name: release/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: release/install-dependencies
|
||||||
|
run: |
|
||||||
|
brew install yq
|
||||||
|
npm ci
|
||||||
|
- name: release/copy-provisioning-profile
|
||||||
|
run: echo $MAS_PROFILE | base64 -D > ./mas.provisionprofile
|
||||||
|
- name: release/patch-version-number-for-MAS
|
||||||
|
run: ./scripts/patch_mas_version.sh
|
||||||
|
- name: release/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: release/package
|
||||||
|
run: npm run package:mas
|
||||||
|
- name: release/publish
|
||||||
|
run: fastlane publish_test path:"$(find . -name \*.pkg -print -quit)"
|
248
.github/workflows/release.yaml
vendored
Normal file
248
.github/workflows/release.yaml
vendored
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||||
|
- "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
env:
|
||||||
|
TERM: xterm
|
||||||
|
MM_WIN_INSTALLERS: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
begin-notification:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: release/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
|
- name: release/fetch-version
|
||||||
|
id: calc
|
||||||
|
run: echo "VERSION=$(jq -r .version package.json)" >> ${GITHUB_OUTPUT}
|
||||||
|
|
||||||
|
- name: release/notify-channel
|
||||||
|
uses: mattermost/action-mattermost-notify@60f5da6e1796b033cf5a038b57031fa011962e27
|
||||||
|
with:
|
||||||
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MM_DESKTOP_RELEASE_WEBHOOK_URL }}
|
||||||
|
MATTERMOST_USERNAME: MattermostRelease
|
||||||
|
MATTERMOST_ICON_URL: https://mattermost.com/wp-content/uploads/2022/02/icon.png
|
||||||
|
TEXT: |
|
||||||
|
[${{ steps.calc.outputs.VERSION }}] Release process for the desktop app has started, it should take about 30 minutes to complete.
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- begin-notification
|
||||||
|
steps:
|
||||||
|
- name: release/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
- name: release/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: release/install-dependencies
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
run: |
|
||||||
|
wget -qO - https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_22.04/Release.key | sudo apt-key add -
|
||||||
|
wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.20.1/yq_linux_amd64 && chmod a+x /usr/local/bin/yq
|
||||||
|
sudo apt-get update || true && sudo apt-get install -y ca-certificates libxtst-dev libpng++-dev gcc-aarch64-linux-gnu g++-aarch64-linux-gnu jq icnsutils graphicsmagick tzdata
|
||||||
|
npm ci
|
||||||
|
- name: release/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: release/build
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/linux
|
||||||
|
npm run package:linux
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/linux
|
||||||
|
- name: release/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-${{ runner.os }}
|
||||||
|
path: ./build
|
||||||
|
compression-level: 0
|
||||||
|
retention-days: 14 ## No need to keep CI builds more than 14 days
|
||||||
|
|
||||||
|
build-msi-installer:
|
||||||
|
runs-on: windows-2022
|
||||||
|
needs:
|
||||||
|
- begin-notification
|
||||||
|
steps:
|
||||||
|
- name: release/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
- name: release/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: release/install-deps
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
choco install yq --version 4.15.1 -y
|
||||||
|
npm i -g node-gyp
|
||||||
|
node-gyp install
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers"
|
||||||
|
node-gyp install --devdir="C:\Users\runneradmin\.electron-gyp" --target=$(jq -r .devDependencies.electron package.json) --dist-url="https://electronjs.org/headers" --arch arm64
|
||||||
|
npm ci --openssl_fips=''
|
||||||
|
- name: release/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: release/build
|
||||||
|
shell: powershell
|
||||||
|
env:
|
||||||
|
MM_WIN_INSTALLERS: 1
|
||||||
|
PFX_KEY: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_PFX_KEY }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_CSC_KEY_PASSWORD }}
|
||||||
|
PFX: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_PFX }}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MSI_INSTALLER_CSC_LINK }}
|
||||||
|
run: |
|
||||||
|
npm run package:windows
|
||||||
|
- name: release/package
|
||||||
|
run: |
|
||||||
|
mkdir -p ./build/win-release
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/win-release
|
||||||
|
- name: release/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-${{ runner.os }}
|
||||||
|
path: ./build
|
||||||
|
compression-level: 0
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
build-mac-installer:
|
||||||
|
runs-on: macos-12
|
||||||
|
needs:
|
||||||
|
- begin-notification
|
||||||
|
steps:
|
||||||
|
- name: release/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
- name: release/setup-node
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version-file: "package.json"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: release/create-build-folder
|
||||||
|
run: mkdir -p ./build
|
||||||
|
- name: release/install-dependencies
|
||||||
|
run: |
|
||||||
|
brew install yq rename
|
||||||
|
npm ci
|
||||||
|
- name: release/test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
- name: release/build
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_APPLE_ID_PASS }}
|
||||||
|
CSC_FOR_PULL_REQUEST: true
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_CSC_KEY_PASSWORD }}
|
||||||
|
CSC_LINK: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_CSC_LINK }}
|
||||||
|
MAC_PROFILE: ${{ secrets.MM_DESKTOP_MAC_INSTALLER_DMG_PROFILE }}
|
||||||
|
run: |
|
||||||
|
echo $MAC_PROFILE | base64 -D > ./mac.provisionprofile
|
||||||
|
mkdir -p ./build/macos-release
|
||||||
|
npm run package:mac-with-universal
|
||||||
|
bash -x ./scripts/patch_updater_yml.sh
|
||||||
|
bash -x ./scripts/cp_artifacts.sh release ./build/macos-release
|
||||||
|
- name: release/rename-arm64-to-m1
|
||||||
|
run: rename 's/arm64/m1/' ./build/macos-release/$(jq -r .version package.json)/*
|
||||||
|
- name: release/upload-build
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: build-${{ runner.os }}
|
||||||
|
path: ./build
|
||||||
|
compression-level: 0
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
upload-to-s3:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- build-mac-installer
|
||||||
|
- build-msi-installer
|
||||||
|
- build-linux
|
||||||
|
steps:
|
||||||
|
- name: release/setup-aws-credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # v1.7.0
|
||||||
|
with:
|
||||||
|
aws-region: us-east-1
|
||||||
|
aws-access-key-id: ${{ secrets.MM_DESKTOP_RELEASE_AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.MM_DESKTOP_RELEASE_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
|
||||||
|
- name: release/download-builds
|
||||||
|
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||||
|
with:
|
||||||
|
path: build
|
||||||
|
pattern: build-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: release/setup-files-for-aws
|
||||||
|
run: |
|
||||||
|
mkdir -p ./aws-s3-dist
|
||||||
|
cp -r --backup=numbered ./build/{macos-release,win-release,linux}/* ./aws-s3-dist
|
||||||
|
- name: release/upload-to-s3
|
||||||
|
run: aws s3 cp ./aws-s3-dist/ s3://releases.mattermost.com/desktop/ --acl public-read --cache-control "no-cache" --recursive
|
||||||
|
|
||||||
|
github-release:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- upload-to-s3
|
||||||
|
steps:
|
||||||
|
- name: release/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
|
- name: release/download-builds
|
||||||
|
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||||
|
with:
|
||||||
|
path: build
|
||||||
|
pattern: build-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: release/setup-files-for-github-release
|
||||||
|
run: |
|
||||||
|
mkdir -p ./ghr-dist
|
||||||
|
find ./build/{macos-release,win-release,linux} -type f -exec cp --backup=numbered -t ./ghr-dist {} +
|
||||||
|
|
||||||
|
- name: release/publish-release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION=$(jq -r .version package.json)
|
||||||
|
./scripts/generate_release_markdown.sh ${VERSION} > release-notes.md
|
||||||
|
RELEASE_TITLE="v${VERSION} ($(date -u "+%Y-%m-%d"))"
|
||||||
|
[[ $VERSION =~ "-rc" ]] && PRERELEASE="--prerelease"
|
||||||
|
gh release create --draft ${PRERELEASE} --verify-tag -F release-notes.md --target "${GITHUB_SHA}" --title "${RELEASE_TITLE}" "${GITHUB_REF_NAME}" ./ghr-dist/*
|
||||||
|
|
||||||
|
end-notification:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- github-release
|
||||||
|
steps:
|
||||||
|
- name: release/checkout-repo
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: release/fetch-version
|
||||||
|
id: calc
|
||||||
|
run: |
|
||||||
|
echo "BODY<<EOF" >> "${GITHUB_OUTPUT}"
|
||||||
|
bash -x scripts/generate_release_post.sh $(jq -r .version package.json) >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "EOF" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: release/notify-channel
|
||||||
|
uses: mattermost/action-mattermost-notify@60f5da6e1796b033cf5a038b57031fa011962e27
|
||||||
|
with:
|
||||||
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MM_DESKTOP_RELEASE_WEBHOOK_URL }}
|
||||||
|
MATTERMOST_USERNAME: MattermostRelease
|
||||||
|
MATTERMOST_ICON_URL: https://mattermost.com/wp-content/uploads/2022/02/icon.png
|
||||||
|
TEXT: |
|
||||||
|
${{ steps.calc.outputs.BODY }}
|
47
.github/workflows/scorecards-analysis.yml
vendored
Normal file
47
.github/workflows/scorecards-analysis.yml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
name: Scorecards supply-chain security
|
||||||
|
on:
|
||||||
|
# Only the default branch is supported.
|
||||||
|
branch_protection_rule:
|
||||||
|
schedule:
|
||||||
|
- cron: "44 7 * * 5"
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
# Declare default permissions as read only.
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analysis:
|
||||||
|
name: Scorecards analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
# Needed if using Code scanning alerts
|
||||||
|
security-events: write
|
||||||
|
# Needed for GitHub OIDC token if publish_results is true
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: "Checkout code"
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: "Run analysis"
|
||||||
|
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
|
||||||
|
with:
|
||||||
|
results_file: results.sarif
|
||||||
|
results_format: sarif
|
||||||
|
publish_results: true
|
||||||
|
|
||||||
|
# Upload the results as artifacts (optional).
|
||||||
|
- name: "Upload artifact"
|
||||||
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
|
with:
|
||||||
|
name: SARIF file
|
||||||
|
path: results.sarif
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
|
- name: "Upload to code-scanning"
|
||||||
|
uses: github/codeql-action/upload-sarif@423a04bb2cb7cd2643007122588f1387778f14d0 # v2.16.5
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
*_bundle.js
|
||||||
|
#release*/
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
dist/
|
||||||
|
mochawesome-report/
|
||||||
|
|
||||||
|
test-results.xml
|
||||||
|
/e2e/performance/perf-test-report.json
|
||||||
|
test_config.json
|
||||||
|
.idea
|
||||||
|
testUserData
|
||||||
|
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
fastlane/README.md
|
||||||
|
fastlane/report.xml
|
||||||
|
|
||||||
|
*.provisionprofile
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
.eslintcache
|
8
.gitlab-ci.yml
Normal file
8
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
include:
|
||||||
|
- project: mattermost/ci/desktop
|
||||||
|
ref: main
|
||||||
|
file: private.yml
|
||||||
|
|
||||||
|
|
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"msjsdiag.debugger-for-chrome",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"lokalise.i18n-ally",
|
||||||
|
"EditorConfig.EditorConfig"
|
||||||
|
]
|
||||||
|
}
|
74
.vscode/launch.json
vendored
Normal file
74
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug Main Process",
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||||
|
"windows": {
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
|
||||||
|
},
|
||||||
|
"program": "${workspaceRoot}/dist/index.js",
|
||||||
|
"preLaunchTask": "Build sources"
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"name": "Debug Renderer Process",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||||
|
"windows": {
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
|
||||||
|
},
|
||||||
|
"runtimeArgs": [
|
||||||
|
"${workspaceRoot}/src",
|
||||||
|
"--disable-dev-mode",
|
||||||
|
"--remote-debugging-port=9222"
|
||||||
|
],
|
||||||
|
"webRoot": "${workspaceRoot}/src/browser",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"preLaunchTask": "Build sources"
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "E2E Tests",
|
||||||
|
"program": "${workspaceRoot}/node_modules/electron-mocha/bin/electron-mocha",
|
||||||
|
"args": [
|
||||||
|
"--recursive",
|
||||||
|
"--timeout",
|
||||||
|
"999999",
|
||||||
|
"--colors",
|
||||||
|
"${workspaceRoot}/dist/tests/e2e_bundle.js"
|
||||||
|
],
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"preLaunchTask": "prepare-e2e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "E2E Performance Tests",
|
||||||
|
"program": "${workspaceRoot}/node_modules/electron-mocha/bin/electron-mocha",
|
||||||
|
"args": [
|
||||||
|
"--recursive",
|
||||||
|
"--timeout",
|
||||||
|
"999999",
|
||||||
|
"--colors",
|
||||||
|
"${workspaceRoot}/dist/tests/e2e_bundle.js"
|
||||||
|
],
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"preLaunchTask": "prepare-e2e-performance",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Unit Tests",
|
||||||
|
"program": "${workspaceRoot}/node_modules/jest/bin/jest",
|
||||||
|
"args": [],
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
55
.vscode/settings.json
vendored
Normal file
55
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"i18n"
|
||||||
|
],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "always",
|
||||||
|
},
|
||||||
|
"cSpell.words": [
|
||||||
|
"appimage",
|
||||||
|
"automations",
|
||||||
|
"Autoupgrade",
|
||||||
|
"browserview",
|
||||||
|
"btns",
|
||||||
|
"chromedriver",
|
||||||
|
"chromedriverlog",
|
||||||
|
"deauthorize",
|
||||||
|
"deeplink",
|
||||||
|
"deeplinking",
|
||||||
|
"diskimage",
|
||||||
|
"dont",
|
||||||
|
"favicons",
|
||||||
|
"filechooser",
|
||||||
|
"flac",
|
||||||
|
"FOCALBOARD",
|
||||||
|
"gsettings",
|
||||||
|
"ICONNAME",
|
||||||
|
"inputflash",
|
||||||
|
"libxtst",
|
||||||
|
"loadscreen",
|
||||||
|
"mailhost",
|
||||||
|
"mailserver",
|
||||||
|
"MMAUTHTOKEN",
|
||||||
|
"MMCSRF",
|
||||||
|
"mmjstool",
|
||||||
|
"MMUSERID",
|
||||||
|
"mochawesome",
|
||||||
|
"NOSERVERS",
|
||||||
|
"Ochiai",
|
||||||
|
"officedocument",
|
||||||
|
"openxmlformats",
|
||||||
|
"presentationml",
|
||||||
|
"servernum",
|
||||||
|
"showunreadbadge",
|
||||||
|
"spreadsheetml",
|
||||||
|
"textbox",
|
||||||
|
"UNCLOSEABLE",
|
||||||
|
"Unmaximize",
|
||||||
|
"Unreads",
|
||||||
|
"webcontents",
|
||||||
|
"wordprocessingml",
|
||||||
|
"xvfb",
|
||||||
|
"Yuya"
|
||||||
|
],
|
||||||
|
"i18n-ally.keystyle": "nested"
|
||||||
|
}
|
25
.vscode/tasks.json
vendored
Normal file
25
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Build sources",
|
||||||
|
"type": "npm",
|
||||||
|
"script": "build",
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "prepare-e2e",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm run build; npm run build-robotjs; npm run test:e2e:build",
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "prepare-e2e-performance",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "cross-env NODE_ENV=test PERFORMANCE=true npm run build; npm run build-robotjs; npm run test:e2e:build-performance",
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Mattermost Desktop Application Changelog
|
||||||
|
|
||||||
|
The Desktop App changelog has moved to the [Admin Documentation](https://docs.mattermost.com/help/apps/desktop-changelog.html).
|
46
CONTRIBUTING.md
Normal file
46
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Contributing Guidelines
|
||||||
|
Thank you for your interest in contributing! Please see the guidelines below before contributing and [join our "Developers: Desktop App" community channel](https://community.mattermost.com/core/channels/desktop-app) to ask questions from community members and the Mattermost Desktop team.
|
||||||
|
|
||||||
|
You can also visit our [developer guide](https://developers.mattermost.com/contribute/desktop/) to learn more information about how to set up your environment, as well as develop and test changes to the Desktop App.
|
||||||
|
|
||||||
|
## Issue
|
||||||
|
We really appreciate your feedback on the Desktop App. We'd ask that before you file an issue that you go through a few steps beforehand.
|
||||||
|
|
||||||
|
### Does it reproduce in a web browser?
|
||||||
|
Mattermost Desktop is based on Electron, which integrates the Chrome engine within a standalone application.
|
||||||
|
If the problem you encounter can be reproduced on web browsers, it may be an issue with Mattermost server (or Chrome).
|
||||||
|
|
||||||
|
If this is the case, please create an issue in the [mattermost-server](https://github.com/mattermost/mattermost-server) or [mattermost-webapp](https://github.com/mattermost/mattermost-webapp) repositories.
|
||||||
|
|
||||||
|
### Try "Clear Cache and Reload"
|
||||||
|
Sometimes issues can be resolved simply by refreshing your Mattermost server within the app.
|
||||||
|
You can do this by pressing `CMD/CTRL+SHIFT+R` in the Mattermost Desktop App, or you can go to the menu and select **View > Clear Cache and Reload**.
|
||||||
|
|
||||||
|
### Write detailed information
|
||||||
|
If the issue still persists, please provide detailed information to help us to understand the problem. Include information such as:
|
||||||
|
* How to reproduce, step-by-step
|
||||||
|
* Expected behavior (or what is wrong)
|
||||||
|
* Screenshots (for GUI issues)
|
||||||
|
* Desktop App version (can be viewed by going to 3-dot menu > Help, or **Menu > Mattermost > About Mattermost** on macOS).
|
||||||
|
* Operating System
|
||||||
|
* Mattermost Server version
|
||||||
|
|
||||||
|
## Feature idea
|
||||||
|
If you have an idea for a new feature, we'd love to hear about it!
|
||||||
|
Please let us know in the Mattermost Community server by making a post in the [Feature Proposals](https://community-daily.mattermost.com/core/channels/feature-ideas) channel.
|
||||||
|
|
||||||
|
## Pull request
|
||||||
|
If you are interested on working on an issue, we would very much appreciate your help!
|
||||||
|
|
||||||
|
We have a list of issues marked as [Help Wanted](https://mattermost.com/pl/help-wanted-desktop) that are available to be worked on.
|
||||||
|
If you'd like to take on an issue, simply comment on the issue and one of the Core Contributors will assign it to you.
|
||||||
|
|
||||||
|
Once your change is ready, please make sure you perform the following tasks before submitting a pull request:
|
||||||
|
1. Make sure that the PR passes all automated checks. You can do this by running the following commands:
|
||||||
|
```
|
||||||
|
npm run lint:js
|
||||||
|
npm run check-types
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
2. If you are fixing a bug, consider writing a unit test for the change so that the issue does not resurface. If you are adding a new feature, consider additionally writing end-to-end (E2E) tests to thoroughly test the changes.
|
||||||
|
3. Please complete the [Mattermost CLA](https://mattermost.com/contribute/) prior to submitting a PR.
|
204
LICENSE.txt
Normal file
204
LICENSE.txt
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
Copyright (c) 2015-2016 Yuya Ochiai
|
||||||
|
Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
55
Makefile
Normal file
55
Makefile
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
SIGNER?="origin"
|
||||||
|
|
||||||
|
GPG=$(shell command which gpg || echo "N/A")
|
||||||
|
DPKG_SIG=$(shell command which dpkg-sig || echo "N/A")
|
||||||
|
|
||||||
|
#define sign_debian_package
|
||||||
|
# dpkg-sig -k ${GPG_KEY_ID} --sign ${SIGNER} $1
|
||||||
|
# dpkg-sig --verify $1
|
||||||
|
#endef
|
||||||
|
|
||||||
|
.PHONY: check-sign-linux-deb
|
||||||
|
check-sign-linux-deb: ##Check running environment to sign debian packages
|
||||||
|
ifeq ("$(GPG)","N/A")
|
||||||
|
@echo "Path does not contain gpg executable. Consider install!"
|
||||||
|
@exit 128
|
||||||
|
else
|
||||||
|
@echo "gpg Found in path!"
|
||||||
|
endif
|
||||||
|
ifeq ("$(DPKG_SIG)","N/A")
|
||||||
|
@echo "Path does not contain dpkg_sig executable. Consider install!"
|
||||||
|
@exit 128
|
||||||
|
else
|
||||||
|
@echo "dpkg_sig Found in path!"
|
||||||
|
endif
|
||||||
|
ifndef GPG_KEY_ID
|
||||||
|
@echo "Please define GPG_KEY_ID environment variable!"
|
||||||
|
@exit 128
|
||||||
|
else
|
||||||
|
@echo "GPG_KEY_ID is defined"
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: npm-ci
|
||||||
|
npm-ci: ## Install all npm dependencies
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci
|
||||||
|
|
||||||
|
.PHONY: package
|
||||||
|
package: package-linux-deb ## Generates packages for all environments
|
||||||
|
|
||||||
|
.PHONY: package-linux-deb
|
||||||
|
package-linux-deb: npm-ci ## Generates linux packages under build/linux folder
|
||||||
|
npm run package:linux-deb
|
||||||
|
mkdir -p artifacts
|
||||||
|
find ./release -name '*.deb' -exec cp "{}" artifacts/ \;
|
||||||
|
|
||||||
|
.PHONY: sign
|
||||||
|
sign: sign-linux-deb ## Sign packages in artifacts directory
|
||||||
|
|
||||||
|
.PHONY: sign-linux-deb
|
||||||
|
sign-linux-deb: check-sign-linux-deb ## Sign debian packages
|
||||||
|
$(foreach file, $(wildcard artifacts/*.deb), $(call sign_debian_package,${file});)
|
||||||
|
|
||||||
|
## Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||||
|
help:
|
||||||
|
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' ./Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
2647
NOTICE.txt
Normal file
2647
NOTICE.txt
Normal file
File diff suppressed because it is too large
Load diff
50
README.md
Normal file
50
README.md
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Mattermost Desktop
|
||||||
|
|
||||||
|
[Mattermost](https://mattermost.com) is an open source platform for secure collaboration across the entire software development lifecycle. This repo is for the native desktop application that's built on [Electron](http://electron.atom.io/); it runs on Windows, Mac, and Linux.
|
||||||
|
|
||||||
|
Originally created as "electron-mattermost" by Yuya Ochiai.
|
||||||
|
|
||||||
|
![mm-desktop-screenshot](https://user-images.githubusercontent.com/52460000/146078917-e1ba8c1f-24e5-4613-8b4b-f3507422f4f2.png)
|
||||||
|
|
||||||
|
[![nightly-builds](https://github.com/mattermost/desktop/actions/workflows/nightly-builds.yaml/badge.svg)](https://github.com/mattermost/desktop/actions/workflows/nightly-builds.yaml)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Desktop integration
|
||||||
|
* Server dropdown for access to multiple servers
|
||||||
|
* Dedicated tabs for Channels, Boards and Playbooks
|
||||||
|
* Desktop Notifications
|
||||||
|
* Badges for unread channels and mentions
|
||||||
|
* Deep Linking to open Mattermost links directly in the app
|
||||||
|
* Runs in background to reduce number of open windows
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
Detailed guides are available at [docs.mattermost.com](https://docs.mattermost.com/install/desktop-app-install.html).
|
||||||
|
|
||||||
|
1. Download a file from the [downloads page](https://mattermost.com/download/#mattermostApps) or from the [releases page](https://github.com/mattermost/desktop/releases).
|
||||||
|
2. Run the installer or unzip the archive.
|
||||||
|
3. Launch Mattermost from your Applications folder, menu, or the unarchived folder.
|
||||||
|
3. On the first launch, please enter a name and URL for your Mattermost server. For example, `https://mattermost.example.com`.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
You can show the dialog from menu bar.
|
||||||
|
|
||||||
|
Configuration will be saved into Electron's userData directory:
|
||||||
|
|
||||||
|
* `%APPDATA%\Mattermost` on Windows
|
||||||
|
* `~/Library/Application Support/Mattermost` on OS X
|
||||||
|
* `~/.config/Mattermost` on Linux
|
||||||
|
|
||||||
|
A custom data directory location can be specified with:
|
||||||
|
|
||||||
|
* `Mattermost.exe --args --data-dir C:\my-mattermost-data` on Windows
|
||||||
|
* `open /Applications/Mattermost.app/ --args --data-dir ~/my-mattermost-data/` on macOS
|
||||||
|
* `./mattermost-desktop --args --data-dir ~/my-mattermost-data/` on Linux
|
||||||
|
|
||||||
|
## Custom App Deployments
|
||||||
|
Our [docs provide a guide](https://docs.mattermost.com/deployment/desktop-app-deployment.html) on how to customize and distribute your own Mattermost Desktop App, including how to distribute the official Windows Desktop App silently to end users, pre-configured with the server URL and other app settings.
|
||||||
|
|
||||||
|
## Development and Making Contributions
|
||||||
|
Our [developer guide](https://developers.mattermost.com/contribute/desktop/) has detailed information on how to set up your development environment, develop, and test changes to the Desktop App.
|
25
SECURITY.md
Normal file
25
SECURITY.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
Security
|
||||||
|
========
|
||||||
|
|
||||||
|
Safety and data security is of the utmost priority for the Mattermost community. If you are a security researcher and have discovered a security vulnerability in our codebase, we would appreciate your help in disclosing it to us in a responsible manner.
|
||||||
|
|
||||||
|
Reporting security issues
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
**Please do not use GitHub issues for security-sensitive communication.**
|
||||||
|
|
||||||
|
Security issues in the community test server, any of the open source codebases maintained by Mattermost, or any of our commercial offerings should be reported via email to [responsibledisclosure@mattermost.com](mailto:responsibledisclosure@mattermost.com). Mattermost is committed to working together with researchers and keeping them updated throughout the patching process. Researchers who responsibly report valid security issues will be publicly credited for their efforts (if they so choose).
|
||||||
|
|
||||||
|
For a more detailed description of the disclosure process and a list of researchers who have previously contributed to the disclosure program, see [Report a Security Vulnerability](https://mattermost.com/security-vulnerability-report/) on the Mattermost website.
|
||||||
|
|
||||||
|
Security updates
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Mattermost has a mandatory upgrade policy, and updates are only provided for the latest release. Critical updates are delivered as dot releases. Details on security updates are announced 30 days after the availability of the update.
|
||||||
|
|
||||||
|
For more details about the security content of past releases, see the [Security Updates](https://mattermost.com/security-updates/) page on the Mattermost website. For timely notifications about new security updates, subscribe to the [Security Bulletins Mailing List](https://mattermost.com/security-updates/#sign-up).
|
||||||
|
|
||||||
|
Contributing to this policy
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
If you have feedback or suggestions on improving this policy document, please [create an issue](https://github.com/mattermost/desktop/issues/new).
|
40
TESTING.md
Normal file
40
TESTING.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# Mattermost Desktop App Testing
|
||||||
|
|
||||||
|
## Automated Testing Guide
|
||||||
|
You can find information about our automated tests in the [developer guide](https://developers.mattermost.com/contribute/desktop/testing/)
|
||||||
|
|
||||||
|
If you are interested in contributing to our automated test library, please read our [contributing guidelines](https://github.com/mattermost/desktop/blob/master/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## Release Testing Guide
|
||||||
|
|
||||||
|
Thank you for your interest in improving Mattermost software prior to its next release. Your bug reports increase the quality of the Mattermost experience for thousands of people around the world using Mattermost.
|
||||||
|
|
||||||
|
New bug reports benefiting the next release will be documented in the release notes to recognize your unique contribution in the history of the Mattermost open source project.
|
||||||
|
|
||||||
|
To contribute to the process of testing the Mattermost Desktop App:
|
||||||
|
|
||||||
|
1. If you haven't already, create an account on our [Community Server](https://community.mattermost.com/)
|
||||||
|
- Set your username to be the same as your GitHub username
|
||||||
|
|
||||||
|
2. Install the latest Mattermost Desktop App
|
||||||
|
- Download the latest pre-release Mattermost Desktop App from the [GitHub Releases page](https://github.com/mattermost/desktop/releases).
|
||||||
|
- Follow the [Desktop Application Install Guides](https://docs.mattermost.com/install/desktop-app-install.html) to install the app for your platform.
|
||||||
|
- Use the [Managing Servers Guide](https://docs.mattermost.com/messaging/managing-desktop-app-servers.html) to add https://community.mattermost.com/core as a new server.
|
||||||
|
- Select "Save" and log in to Mattermost.
|
||||||
|
|
||||||
|
3. Go to the [Public Test Channel](https://community.mattermost.com/core/channels/public-test-channel) and try the following:
|
||||||
|
- Post a message with information on what you're testing, for example: `Testing Mattermost Desktop App 5.0.2 on Windows 10 64-bit`.
|
||||||
|
- Reply to the post by clicking on "..." then "Reply" with This is a comment including files and upload five (5) files including at least one image, one sound file and one video clip from your Desktop App.
|
||||||
|
- Search for the word "Desktop" and click "Jump" on the search result of your own post in Step 3.1. Click into the preview of the files you uploaded and try to download each one.
|
||||||
|
- Verify [Server Management works as documented](https://docs.mattermost.com/messaging/managing-desktop-app-servers.html).
|
||||||
|
- Verify [App Options work as documented](https://docs.mattermost.com/messaging/managing-desktop-app-options.html).
|
||||||
|
- Verify Menu Bar options work as documented.
|
||||||
|
- Use the desktop app for another 15 minutes, trying different features and functionality on the user interface.
|
||||||
|
|
||||||
|
4. For any bugs found, please [file a new issue report for each](https://github.com/mattermost/desktop/issues/new).
|
||||||
|
- Please include:
|
||||||
|
- Operating System
|
||||||
|
- Mattermost Desktop App version (See File Menu > Help > Version Number)
|
||||||
|
- Mattermost Server version (See Mattermost Menu > About Mattermost, where Mattermost Menu can be accessed by clicking on three dots next to your profile name)
|
||||||
|
- Clear steps to reproduce the issue
|
||||||
|
- [See example of Desktop App issue](https://github.com/mattermost/desktop/issues/355)
|
84
api-types/index.ts
Normal file
84
api-types/index.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export type DesktopSourcesOptions = {
|
||||||
|
types: Array<'screen' | 'window'>;
|
||||||
|
thumbnailSize?: {height: number; width: number};
|
||||||
|
fetchWindowIcons?: boolean;
|
||||||
|
};
|
||||||
|
export type DesktopCaptureSource = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
thumbnailURL: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DesktopAPI = {
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
isDev: () => Promise<boolean>;
|
||||||
|
getAppInfo: () => Promise<{name: string; version: string}>;
|
||||||
|
reactAppInitialized: () => void;
|
||||||
|
|
||||||
|
// Session
|
||||||
|
setSessionExpired: (isExpired: boolean) => void;
|
||||||
|
onUserActivityUpdate: (listener: (
|
||||||
|
userIsActive: boolean,
|
||||||
|
idleTime: number,
|
||||||
|
isSystemEvent: boolean,
|
||||||
|
) => void) => () => void;
|
||||||
|
onLogin: () => void;
|
||||||
|
onLogout: () => void;
|
||||||
|
|
||||||
|
// Unreads/mentions/notifications
|
||||||
|
sendNotification: (title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) => Promise<{status: string; reason?: string; data?: string}>;
|
||||||
|
onNotificationClicked: (listener: (channelId: string, teamId: string, url: string) => void) => () => void;
|
||||||
|
setUnreadsAndMentions: (isUnread: boolean, mentionCount: number) => void;
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
requestBrowserHistoryStatus: () => Promise<{canGoBack: boolean; canGoForward: boolean}>;
|
||||||
|
onBrowserHistoryStatusUpdated: (listener: (canGoBack: boolean, canGoForward: boolean) => void) => () => void;
|
||||||
|
onBrowserHistoryPush: (listener: (pathName: string) => void) => () => void;
|
||||||
|
sendBrowserHistoryPush: (path: string) => void;
|
||||||
|
|
||||||
|
// Calls
|
||||||
|
joinCall: (opts: {
|
||||||
|
callID: string;
|
||||||
|
title: string;
|
||||||
|
rootID: string;
|
||||||
|
channelURL: string;
|
||||||
|
}) => Promise<{callID: string; sessionID: string}>;
|
||||||
|
leaveCall: () => void;
|
||||||
|
|
||||||
|
callsWidgetConnected: (callID: string, sessionID: string) => void;
|
||||||
|
resizeCallsWidget: (width: number, height: number) => void;
|
||||||
|
|
||||||
|
sendCallsError: (err: string, callID?: string, errMsg?: string) => void;
|
||||||
|
onCallsError: (listener: (err: string, callID?: string, errMsg?: string) => void) => () => void;
|
||||||
|
|
||||||
|
getDesktopSources: (opts: DesktopSourcesOptions) => Promise<DesktopCaptureSource[]>;
|
||||||
|
|
||||||
|
openScreenShareModal: () => void;
|
||||||
|
onOpenScreenShareModal: (listener: () => void) => () => void;
|
||||||
|
|
||||||
|
shareScreen: (sourceID: string, withAudio: boolean) => void;
|
||||||
|
onScreenShared: (listener: (sourceID: string, withAudio: boolean) => void) => () => void;
|
||||||
|
|
||||||
|
sendJoinCallRequest: (callId: string) => void;
|
||||||
|
onJoinCallRequest: (listener: (callID: string) => void) => () => void;
|
||||||
|
|
||||||
|
openLinkFromCalls: (url: string) => void;
|
||||||
|
|
||||||
|
focusPopout: () => void;
|
||||||
|
|
||||||
|
openThreadForCalls: (threadID: string) => void;
|
||||||
|
onOpenThreadForCalls: (listener: (threadID: string) => void) => () => void;
|
||||||
|
|
||||||
|
openStopRecordingModal: (channelID: string) => void;
|
||||||
|
onOpenStopRecordingModal: (listener: (channelID: string) => void) => () => void;
|
||||||
|
|
||||||
|
openCallsUserSettings: () => void;
|
||||||
|
onOpenCallsUserSettings: (listener: () => void) => () => void;
|
||||||
|
|
||||||
|
// Utility
|
||||||
|
unregister: (channel: string) => void;
|
||||||
|
}
|
69
api-types/lib/index.d.ts
vendored
Normal file
69
api-types/lib/index.d.ts
vendored
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
export type DesktopSourcesOptions = {
|
||||||
|
types: Array<'screen' | 'window'>;
|
||||||
|
thumbnailSize?: {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
fetchWindowIcons?: boolean;
|
||||||
|
};
|
||||||
|
export type DesktopCaptureSource = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
thumbnailURL: string;
|
||||||
|
};
|
||||||
|
export type DesktopAPI = {
|
||||||
|
isDev: () => Promise<boolean>;
|
||||||
|
getAppInfo: () => Promise<{
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
}>;
|
||||||
|
reactAppInitialized: () => void;
|
||||||
|
setSessionExpired: (isExpired: boolean) => void;
|
||||||
|
onUserActivityUpdate: (listener: (userIsActive: boolean, idleTime: number, isSystemEvent: boolean) => void) => () => void;
|
||||||
|
onLogin: () => void;
|
||||||
|
onLogout: () => void;
|
||||||
|
sendNotification: (title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) => Promise<{
|
||||||
|
status: string;
|
||||||
|
reason?: string;
|
||||||
|
data?: string;
|
||||||
|
}>;
|
||||||
|
onNotificationClicked: (listener: (channelId: string, teamId: string, url: string) => void) => () => void;
|
||||||
|
setUnreadsAndMentions: (isUnread: boolean, mentionCount: number) => void;
|
||||||
|
requestBrowserHistoryStatus: () => Promise<{
|
||||||
|
canGoBack: boolean;
|
||||||
|
canGoForward: boolean;
|
||||||
|
}>;
|
||||||
|
onBrowserHistoryStatusUpdated: (listener: (canGoBack: boolean, canGoForward: boolean) => void) => () => void;
|
||||||
|
onBrowserHistoryPush: (listener: (pathName: string) => void) => () => void;
|
||||||
|
sendBrowserHistoryPush: (path: string) => void;
|
||||||
|
joinCall: (opts: {
|
||||||
|
callID: string;
|
||||||
|
title: string;
|
||||||
|
rootID: string;
|
||||||
|
channelURL: string;
|
||||||
|
}) => Promise<{
|
||||||
|
callID: string;
|
||||||
|
sessionID: string;
|
||||||
|
}>;
|
||||||
|
leaveCall: () => void;
|
||||||
|
callsWidgetConnected: (callID: string, sessionID: string) => void;
|
||||||
|
resizeCallsWidget: (width: number, height: number) => void;
|
||||||
|
sendCallsError: (err: string, callID?: string, errMsg?: string) => void;
|
||||||
|
onCallsError: (listener: (err: string, callID?: string, errMsg?: string) => void) => () => void;
|
||||||
|
getDesktopSources: (opts: DesktopSourcesOptions) => Promise<DesktopCaptureSource[]>;
|
||||||
|
openScreenShareModal: () => void;
|
||||||
|
onOpenScreenShareModal: (listener: () => void) => () => void;
|
||||||
|
shareScreen: (sourceID: string, withAudio: boolean) => void;
|
||||||
|
onScreenShared: (listener: (sourceID: string, withAudio: boolean) => void) => () => void;
|
||||||
|
sendJoinCallRequest: (callId: string) => void;
|
||||||
|
onJoinCallRequest: (listener: (callID: string) => void) => () => void;
|
||||||
|
openLinkFromCalls: (url: string) => void;
|
||||||
|
focusPopout: () => void;
|
||||||
|
openThreadForCalls: (threadID: string) => void;
|
||||||
|
onOpenThreadForCalls: (listener: (threadID: string) => void) => () => void;
|
||||||
|
openStopRecordingModal: (channelID: string) => void;
|
||||||
|
onOpenStopRecordingModal: (listener: (channelID: string) => void) => () => void;
|
||||||
|
openCallsUserSettings: () => void;
|
||||||
|
onOpenCallsUserSettings: (listener: () => void) => () => void;
|
||||||
|
unregister: (channel: string) => void;
|
||||||
|
};
|
4
api-types/lib/index.js
Normal file
4
api-types/lib/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"use strict";
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
21
api-types/package-lock.json
generated
Normal file
21
api-types/package-lock.json
generated
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "@mattermost/desktop-api",
|
||||||
|
"version": "5.10.0-1",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@mattermost/desktop-api",
|
||||||
|
"version": "5.10.0-1",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^4.3.0 || ^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
api-types/package.json
Normal file
34
api-types/package.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "@mattermost/desktop-api",
|
||||||
|
"version": "5.10.0-1",
|
||||||
|
"description": "Shared types for the Desktop App API provided to the Web App",
|
||||||
|
"keywords": [
|
||||||
|
"mattermost"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/mattermost/desktop",
|
||||||
|
"license": "MIT",
|
||||||
|
"files": [
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "github:mattermost/desktop",
|
||||||
|
"directory": "api-types"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^4.3.0 || ^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build --verbose",
|
||||||
|
"run": "tsc --watch --preserveWatchOutput",
|
||||||
|
"clean": "rm -rf tsconfig.tsbuildinfo ./lib",
|
||||||
|
"prepublishOnly": "npm run build && rm -rf ./lib/tsconfig.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
21
api-types/tsconfig.json
Normal file
21
api-types/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "es6",
|
||||||
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"outDir": "./lib",
|
||||||
|
"rootDir": ".",
|
||||||
|
"composite": true,
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./index.ts"
|
||||||
|
]
|
||||||
|
}
|
27
babel.config.js
Normal file
27
babel.config.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
module.exports = (api) => {
|
||||||
|
api.cache.forever();
|
||||||
|
return {
|
||||||
|
presets: [
|
||||||
|
['@babel/preset-env', {
|
||||||
|
targets: {
|
||||||
|
browsers: ['Electron >= 29.0'],
|
||||||
|
node: '20.9',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
'@babel/preset-react',
|
||||||
|
['@babel/typescript', {
|
||||||
|
allExtensions: true,
|
||||||
|
isTSX: true,
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-transform-object-rest-spread',
|
||||||
|
'@babel/plugin-transform-class-properties',
|
||||||
|
'@babel/plugin-transform-private-methods',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
11
e2e/babel.config.js
Normal file
11
e2e/babel.config.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
module.exports = (api) => {
|
||||||
|
api.cache.forever();
|
||||||
|
return {
|
||||||
|
presets: [
|
||||||
|
'@babel/typescript',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
330
e2e/modules/environment.js
Normal file
330
e2e/modules/environment.js
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const {execSync} = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const chai = require('chai');
|
||||||
|
const {ipcRenderer} = require('electron');
|
||||||
|
const {_electron: electron} = require('playwright');
|
||||||
|
const ps = require('ps-node');
|
||||||
|
const {SHOW_SETTINGS_WINDOW} = require('src/common/communication');
|
||||||
|
|
||||||
|
const {asyncSleep, mkDirAsync, rmDirAsync, unlinkAsync} = require('./utils');
|
||||||
|
chai.should();
|
||||||
|
|
||||||
|
const sourceRootDir = path.join(__dirname, '../..');
|
||||||
|
const electronBinaryPath = (() => {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
return path.join(sourceRootDir, 'node_modules/electron/dist/Electron.app/Contents/MacOS/Electron');
|
||||||
|
}
|
||||||
|
const exeExtension = (process.platform === 'win32') ? '.exe' : '';
|
||||||
|
return path.join(sourceRootDir, 'node_modules/electron/dist/electron' + exeExtension);
|
||||||
|
})();
|
||||||
|
const userDataDir = path.join(sourceRootDir, 'e2e/testUserData/');
|
||||||
|
const configFilePath = path.join(userDataDir, 'config.json');
|
||||||
|
const downloadsFilePath = path.join(userDataDir, 'downloads.json');
|
||||||
|
const downloadsLocation = path.join(userDataDir, 'Downloads');
|
||||||
|
const boundsInfoPath = path.join(userDataDir, 'bounds-info.json');
|
||||||
|
const appUpdatePath = path.join(userDataDir, 'app-update.yml');
|
||||||
|
const exampleURL = 'http://example.com/';
|
||||||
|
const mattermostURL = process.env.MM_TEST_SERVER_URL || 'http://localhost:8065/';
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const robot = require('robotjs');
|
||||||
|
robot.mouseClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
const exampleServer = {
|
||||||
|
name: 'example',
|
||||||
|
url: exampleURL,
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
};
|
||||||
|
const githubServer = {
|
||||||
|
name: 'github',
|
||||||
|
url: 'https://github.com/',
|
||||||
|
order: 1,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const demoConfig = {
|
||||||
|
version: 3,
|
||||||
|
teams: [exampleServer, githubServer],
|
||||||
|
showTrayIcon: false,
|
||||||
|
trayIconTheme: 'light',
|
||||||
|
minimizeToTray: false,
|
||||||
|
notifications: {
|
||||||
|
flashWindow: 0,
|
||||||
|
bounceIcon: false,
|
||||||
|
bounceIconType: 'informational',
|
||||||
|
},
|
||||||
|
showUnreadBadge: true,
|
||||||
|
useSpellChecker: true,
|
||||||
|
enableHardwareAcceleration: true,
|
||||||
|
autostart: true,
|
||||||
|
hideOnStart: false,
|
||||||
|
spellCheckerLocales: [],
|
||||||
|
darkMode: false,
|
||||||
|
lastActiveTeam: 0,
|
||||||
|
startInFullscreen: false,
|
||||||
|
autoCheckForUpdates: true,
|
||||||
|
appLanguage: 'en',
|
||||||
|
logLevel: 'silly',
|
||||||
|
};
|
||||||
|
|
||||||
|
const demoMattermostConfig = {
|
||||||
|
...demoConfig,
|
||||||
|
teams: [{
|
||||||
|
...exampleServer,
|
||||||
|
url: mattermostURL,
|
||||||
|
}, githubServer],
|
||||||
|
};
|
||||||
|
|
||||||
|
const cmdOrCtrl = process.platform === 'darwin' ? 'command' : 'control';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sourceRootDir,
|
||||||
|
configFilePath,
|
||||||
|
downloadsFilePath,
|
||||||
|
downloadsLocation,
|
||||||
|
userDataDir,
|
||||||
|
boundsInfoPath,
|
||||||
|
appUpdatePath,
|
||||||
|
exampleURL,
|
||||||
|
mattermostURL,
|
||||||
|
demoConfig,
|
||||||
|
demoMattermostConfig,
|
||||||
|
cmdOrCtrl,
|
||||||
|
|
||||||
|
async clearElectronInstances() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
ps.lookup({
|
||||||
|
command: process.platform === 'darwin' ? 'Electron' : 'electron',
|
||||||
|
}, (err, resultList) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resultList.forEach((process) => {
|
||||||
|
if (process && process.command === electronBinaryPath && !process.arguments.some((arg) => arg.includes('electron-mocha'))) {
|
||||||
|
ps.kill(process.pid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanTestConfig() {
|
||||||
|
[configFilePath, downloadsFilePath, boundsInfoPath].forEach((file) => {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(file);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async cleanTestConfigAsync() {
|
||||||
|
await Promise.all(
|
||||||
|
[configFilePath, downloadsFilePath, boundsInfoPath].map((file) => {
|
||||||
|
return unlinkAsync(file);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanDataDir() {
|
||||||
|
try {
|
||||||
|
fs.rmdirSync(userDataDir, {recursive: true});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanDataDirAsync() {
|
||||||
|
return rmDirAsync(userDataDir);
|
||||||
|
},
|
||||||
|
|
||||||
|
createTestUserDataDir() {
|
||||||
|
if (!fs.existsSync(userDataDir)) {
|
||||||
|
fs.mkdirSync(userDataDir);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clipboard(textToCopy) {
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'linux':
|
||||||
|
execSync(`echo "${textToCopy}" | xsel --clipboard`);
|
||||||
|
break;
|
||||||
|
case 'win32':
|
||||||
|
execSync(`echo ${textToCopy} | clip`);
|
||||||
|
break;
|
||||||
|
case 'darwin':
|
||||||
|
execSync(`pbcopy <<< ${textToCopy}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTestUserDataDirAsync() {
|
||||||
|
await mkDirAsync(userDataDir);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getApp(args = []) {
|
||||||
|
const options = {
|
||||||
|
downloadsPath: downloadsLocation,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
RESOURCES_PATH: userDataDir,
|
||||||
|
},
|
||||||
|
executablePath: electronBinaryPath,
|
||||||
|
args: [`${path.join(sourceRootDir, 'e2e/dist')}`, `--user-data-dir=${userDataDir}`, '--disable-dev-mode', '--no-sandbox', ...args],
|
||||||
|
};
|
||||||
|
|
||||||
|
// if (process.env.MM_DEBUG_SETTINGS) {
|
||||||
|
// options.chromeDriverLogPath = './chromedriverlog.txt';
|
||||||
|
// }
|
||||||
|
// if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||||
|
// // on a mac, debugging port might conflict with other apps
|
||||||
|
// // this changes the default debugging port so chromedriver can run without issues.
|
||||||
|
// options.chromeDriverArgs.push('remote-debugging-port=9222');
|
||||||
|
//}
|
||||||
|
return electron.launch(options).then(async (eapp) => {
|
||||||
|
await eapp.evaluate(async ({app}) => {
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
app.on('e2e-app-loaded', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
return eapp;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getServerMap(app) {
|
||||||
|
const map = {};
|
||||||
|
await Promise.all(app.windows().map(async (win) => {
|
||||||
|
return win.evaluate(async () => {
|
||||||
|
if (!window.testHelper) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const info = await window.testHelper.getViewInfoForTest();
|
||||||
|
return {viewName: `${info.serverName}___${info.viewType}`, webContentsId: info.webContentsId};
|
||||||
|
}).then((result) => {
|
||||||
|
if (result) {
|
||||||
|
map[result.viewName] = {win, webContentsId: result.webContentsId};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
return map;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loginToMattermost(window) {
|
||||||
|
// Do this twice because sometimes the app likes to load the login screen, then go to Loading... again
|
||||||
|
await asyncSleep(1000);
|
||||||
|
await window.waitForSelector('#input_loginId');
|
||||||
|
await window.waitForSelector('#input_password-input');
|
||||||
|
await window.waitForSelector('#saveSetting');
|
||||||
|
|
||||||
|
await window.type('#input_loginId', process.env.MM_TEST_USER_NAME);
|
||||||
|
await window.type('#input_password-input', process.env.MM_TEST_PASSWORD);
|
||||||
|
await window.click('#saveSetting');
|
||||||
|
},
|
||||||
|
|
||||||
|
async openDownloadsDropdown(app) {
|
||||||
|
const mainWindow = app.windows().find((window) => window.url().includes('index'));
|
||||||
|
await mainWindow.waitForLoadState();
|
||||||
|
await mainWindow.bringToFront();
|
||||||
|
|
||||||
|
const dlButtonLocator = await mainWindow.waitForSelector('.DownloadsDropdownButton');
|
||||||
|
await dlButtonLocator.click();
|
||||||
|
await asyncSleep(500);
|
||||||
|
|
||||||
|
const downloadsWindow = app.windows().find((window) => window.url().includes('downloadsDropdown.html'));
|
||||||
|
await downloadsWindow.waitForLoadState();
|
||||||
|
await downloadsWindow.bringToFront();
|
||||||
|
await downloadsWindow.isVisible('.DownloadsDropdown');
|
||||||
|
return downloadsWindow;
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadsDropdownIsOpen(app) {
|
||||||
|
const downloadsWindow = app.windows().find((window) => window.url().includes('downloadsDropdown.html'));
|
||||||
|
const result = await downloadsWindow.isVisible('.DownloadsDropdown');
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
addClientCommands(client) {
|
||||||
|
client.addCommand('loadSettingsPage', function async() {
|
||||||
|
ipcRenderer.send(SHOW_SETTINGS_WINDOW);
|
||||||
|
});
|
||||||
|
client.addCommand('isNodeEnabled', function async() {
|
||||||
|
return this.execute(() => {
|
||||||
|
try {
|
||||||
|
if (require('child_process')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).then((requireResult) => {
|
||||||
|
return requireResult.value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
client.addCommand('waitForAppOptionsAutoSaved', function async() {
|
||||||
|
const ID_APP_OPTIONS_SAVE_INDICATOR = '#appOptionsSaveIndicator';
|
||||||
|
const TIMEOUT = 5000;
|
||||||
|
return this.
|
||||||
|
waitForVisible(ID_APP_OPTIONS_SAVE_INDICATOR, TIMEOUT).
|
||||||
|
waitForVisible(ID_APP_OPTIONS_SAVE_INDICATOR, TIMEOUT, true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// execute the test only when `condition` is true
|
||||||
|
shouldTest(it, condition) {
|
||||||
|
return condition ? it : it.skip;
|
||||||
|
},
|
||||||
|
isOneOf(platforms) {
|
||||||
|
return (platforms.indexOf(process.platform) !== -1);
|
||||||
|
},
|
||||||
|
};
|
20
e2e/modules/test.html
Normal file
20
e2e/modules/test.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Mattermost Desktop testing html</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>window.open() test</h1>
|
||||||
|
<p>
|
||||||
|
<input type="button" value="window.open()" onclick="open_window()">
|
||||||
|
</p>
|
||||||
|
<script>
|
||||||
|
function open_window() {
|
||||||
|
window.open('./test.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
104
e2e/modules/utils.js
Normal file
104
e2e/modules/utils.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
function asyncSleep(timeout) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
}, timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dirExistsAsync(path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.stat(path, (error, stats) => {
|
||||||
|
if (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
resolve(false);
|
||||||
|
} else {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(stats.isDirectory());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkDirAsync(path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
dirExistsAsync(path).then((exists) => {
|
||||||
|
if (exists) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fs.mkdir(path, {recursive: true}, (error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function rmDirAsync(path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
dirExistsAsync(path).then((exists) => {
|
||||||
|
if (exists) {
|
||||||
|
fs.rm(path, {recursive: true, force: true}, (error) => {
|
||||||
|
if (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}).catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlinkAsync(path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.unlink(path, (error) => {
|
||||||
|
if (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFileAsync(path, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.writeFile(path, data, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
asyncSleep,
|
||||||
|
dirExistsAsync,
|
||||||
|
mkDirAsync,
|
||||||
|
rmDirAsync,
|
||||||
|
unlinkAsync,
|
||||||
|
writeFileAsync,
|
||||||
|
};
|
4847
e2e/package-lock.json
generated
Normal file
4847
e2e/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
43
e2e/package.json
Normal file
43
e2e/package.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "desktop-e2e",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "E2E tests for the Desktop App",
|
||||||
|
"main": "dist/e2e_bundle.js",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -rf dist/ mochawesome-report/ node_modules/ testUserData/",
|
||||||
|
"run:e2e": "npm run build && npm run test",
|
||||||
|
"build": "webpack-cli --config webpack.config.js",
|
||||||
|
"build:performance": "webpack-cli --config webpack.config.performance.js",
|
||||||
|
"test": "electron-mocha --reporter mochawesome dist/e2e_bundle.js",
|
||||||
|
"test:performance": "electron-mocha --reporter json --reporter-option output=./performance/perf-test-report.json dist/e2e_bundle.js",
|
||||||
|
"send-report": "node ./save_report.js",
|
||||||
|
"postinstall": "cross-env CL='/std:c++17' electron-rebuild -m ./node_modules/robotjs"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+ssh://git@github.com/mattermost/desktop.git"
|
||||||
|
},
|
||||||
|
"author": "Mattermost, Inc. <feedback@mattermost.com>",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/mattermost/desktop/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/mattermost/desktop#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "3.529.0",
|
||||||
|
"@aws-sdk/lib-storage": "3.445.0",
|
||||||
|
"@electron/rebuild": "3.6.0",
|
||||||
|
"axios": "1.7.4",
|
||||||
|
"chai": "4.3.6",
|
||||||
|
"electron-mocha": "12.2.0",
|
||||||
|
"fast-xml-parser": "^4.4.1",
|
||||||
|
"mochawesome": "7.1.3",
|
||||||
|
"playwright": "1.42.0",
|
||||||
|
"ps-node": "0.1.6",
|
||||||
|
"recursive-readdir": "2.2.3",
|
||||||
|
"robotjs": "0.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"mochawesome-report-generator": "^6.2.0"
|
||||||
|
}
|
||||||
|
}
|
29
e2e/performance/app.test.js
Normal file
29
e2e/performance/app.test.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const env = require('../modules/environment');
|
||||||
|
|
||||||
|
describe('startup/app', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
this.app = await env.getApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the welcome screen modal when no servers exist', async () => {
|
||||||
|
const welcomeScreenModal = this.app.windows().find((window) => window.url().includes('welcomeScreen'));
|
||||||
|
const modalButton = await welcomeScreenModal.innerText('.WelcomeScreen .WelcomeScreen__button');
|
||||||
|
modalButton.should.equal('Get Started');
|
||||||
|
});
|
||||||
|
});
|
105
e2e/save_report.js
Normal file
105
e2e/save_report.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is used for saving artifacts to AWS S3, sending data to automation dashboard and
|
||||||
|
* publishing quick summary to community channels.
|
||||||
|
*
|
||||||
|
* Usage: [ENV] node save_report.js
|
||||||
|
*
|
||||||
|
* Environment variables:
|
||||||
|
* BRANCH=[branch] : Branch identifier from CI
|
||||||
|
* BUILD_ID=[build_id] : Build identifier from CI
|
||||||
|
* BUILD_TAG=[build_tag] : Docker image used to run the test
|
||||||
|
*
|
||||||
|
* For saving artifacts to AWS S3
|
||||||
|
* - AWS_S3_BUCKET, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
|
||||||
|
* For saving test cases to Test Management
|
||||||
|
* - ZEPHYR_ENABLE=true|false
|
||||||
|
* - ZEPHYR_API_KEY=[api_key]
|
||||||
|
* - JIRA_PROJECT_KEY=[project_key], e.g. "MM",
|
||||||
|
* - ZEPHYR_FOLDER_ID=[folder_id], e.g. 847997
|
||||||
|
* For sending hooks to Mattermost channels
|
||||||
|
* - FULL_REPORT, WEBHOOK_URL and DIAGNOSTIC_WEBHOOK_URL
|
||||||
|
* Test type
|
||||||
|
* - TYPE=[type], e.g. "MASTER", "PR", "RELEASE", "CLOUD"
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const generator = require('mochawesome-report-generator');
|
||||||
|
|
||||||
|
const {saveArtifacts} = require('./utils/artifacts');
|
||||||
|
const {MOCHAWESOME_REPORT_DIR} = require('./utils/constants');
|
||||||
|
const {
|
||||||
|
generateShortSummary,
|
||||||
|
generateTestReport,
|
||||||
|
removeOldGeneratedReports,
|
||||||
|
sendReport,
|
||||||
|
readJsonFromFile,
|
||||||
|
writeJsonToFile,
|
||||||
|
} = require('./utils/report');
|
||||||
|
const {createTestCycle, createTestExecutions} = require('./utils/test_cases');
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const saveReport = async () => {
|
||||||
|
const {
|
||||||
|
BRANCH,
|
||||||
|
BUILD_ID,
|
||||||
|
BUILD_TAG,
|
||||||
|
ZEPHYR_ENABLE,
|
||||||
|
ZEPHYR_CYCLE_KEY,
|
||||||
|
TYPE,
|
||||||
|
WEBHOOK_URL,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
removeOldGeneratedReports();
|
||||||
|
|
||||||
|
// Import
|
||||||
|
const jsonReport = readJsonFromFile(path.join(MOCHAWESOME_REPORT_DIR, 'mochawesome.json'));
|
||||||
|
|
||||||
|
// Generate the html report file
|
||||||
|
await generator.create(
|
||||||
|
jsonReport,
|
||||||
|
{
|
||||||
|
reportDir: MOCHAWESOME_REPORT_DIR,
|
||||||
|
reportTitle: `Desktop E2E - Build: ${BUILD_ID} Branch: ${BRANCH} Tag: ${BUILD_TAG}`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate short summary, write to file and then send report via webhook
|
||||||
|
const {stats, statsFieldValue} = generateShortSummary(jsonReport);
|
||||||
|
const summary = {stats, statsFieldValue};
|
||||||
|
console.log(summary);
|
||||||
|
writeJsonToFile(summary, 'summary.json', MOCHAWESOME_REPORT_DIR);
|
||||||
|
|
||||||
|
const result = await saveArtifacts();
|
||||||
|
if (result && result.success) {
|
||||||
|
console.log('Successfully uploaded artifacts to S3:', result.reportLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or use an existing test cycle
|
||||||
|
let testCycle = {};
|
||||||
|
if (ZEPHYR_ENABLE === 'true') {
|
||||||
|
const {start, end} = jsonReport.stats;
|
||||||
|
testCycle = ZEPHYR_CYCLE_KEY ? {key: ZEPHYR_CYCLE_KEY} : await createTestCycle(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send test report to "QA: UI Test Automation" channel via webhook
|
||||||
|
if (TYPE && TYPE !== 'NONE' && WEBHOOK_URL) {
|
||||||
|
const data = generateTestReport(summary, result && result.success, result && result.reportLink, testCycle.key);
|
||||||
|
await sendReport('summary report to Community channel', WEBHOOK_URL, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save test cases to Test Management
|
||||||
|
if (ZEPHYR_ENABLE === 'true') {
|
||||||
|
await createTestExecutions(jsonReport, testCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// chai.expect(Boolean(jsonReport.stats.failures), FAILURE_MESSAGE).to.be.false;
|
||||||
|
};
|
||||||
|
|
||||||
|
saveReport();
|
54
e2e/specs/deep_linking/deeplink.test.js
Normal file
54
e2e/specs/deep_linking/deeplink.test.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('application', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
try {
|
||||||
|
await this.app.close();
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
it('MM-T1304/MM-T1306 should open the app on the requested deep link', async () => {
|
||||||
|
this.app = await env.getApp(['mattermost://github.com/test/url']);
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
if (!this.app.windows().some((window) => window.url().includes('github.com'))) {
|
||||||
|
await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('github.com'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const webContentsId = this.serverMap[`${config.teams[1].name}___TAB_MESSAGING`].webContentsId;
|
||||||
|
const isActive = await browserWindow.evaluate((window, id) => {
|
||||||
|
return window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getURL();
|
||||||
|
}, webContentsId);
|
||||||
|
isActive.should.equal('https://github.com/test/url/');
|
||||||
|
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton');
|
||||||
|
dropdownButtonText.should.equal('github');
|
||||||
|
await this.app.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
193
e2e/specs/downloads/downloads_dropdown_items.test.js
Normal file
193
e2e/specs/downloads/downloads_dropdown_items.test.js
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep, mkDirAsync, rmDirAsync, writeFileAsync} = require('../../modules/utils');
|
||||||
|
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
const file1 = {
|
||||||
|
addedAt: Date.UTC(2022, 8, 8, 10), // Aug 08, 2022 10:00AM UTC
|
||||||
|
filename: 'file1.txt',
|
||||||
|
mimeType: 'plain/text',
|
||||||
|
location: path.join(env.downloadsLocation, 'file1.txt'),
|
||||||
|
progress: 100,
|
||||||
|
receivedBytes: 3917388,
|
||||||
|
state: 'completed',
|
||||||
|
totalBytes: 3917388,
|
||||||
|
type: 'file',
|
||||||
|
};
|
||||||
|
const file2 = {
|
||||||
|
addedAt: Date.UTC(2022, 8, 8, 11), // Aug 08, 2022 11:00AM UTC
|
||||||
|
filename: 'file2.txt',
|
||||||
|
mimeType: 'plain/text',
|
||||||
|
location: path.join(env.downloadsLocation, 'file2.txt'),
|
||||||
|
progress: 100,
|
||||||
|
receivedBytes: 7917388,
|
||||||
|
state: 'completed',
|
||||||
|
totalBytes: 7917388,
|
||||||
|
type: 'file',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('downloads/downloads_dropdown_items', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
describe('The list has one downloaded file', () => {
|
||||||
|
const downloads = {
|
||||||
|
[file1.filename]: file1,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await env.createTestUserDataDirAsync();
|
||||||
|
await env.cleanTestConfigAsync();
|
||||||
|
await writeFileAsync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
|
||||||
|
await mkDirAsync(env.downloadsLocation);
|
||||||
|
await writeFileAsync(path.join(env.downloadsLocation, 'file1.txt'), 'file1 content');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.downloadsWindow = await env.openDownloadsDropdown(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rmDirAsync(env.downloadsLocation);
|
||||||
|
await this.app?.close?.();
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-22239 should display the file correctly (downloaded)', async () => {
|
||||||
|
const filenameTextLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__Filename');
|
||||||
|
const filenameInnerText = await filenameTextLocator.innerText();
|
||||||
|
filenameInnerText.should.equal(downloads['file1.txt'].filename);
|
||||||
|
|
||||||
|
const fileStateLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__FileSizeAndStatus');
|
||||||
|
const fileStateInnerText = await fileStateLocator.innerText();
|
||||||
|
fileStateInnerText.should.equal('3.92 MB • Downloaded');
|
||||||
|
|
||||||
|
const fileThumbnailLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__Thumbnail');
|
||||||
|
const thumbnailBackgroundImage = await fileThumbnailLocator.evaluate((node) => window.getComputedStyle(node).getPropertyValue('background-image'));
|
||||||
|
thumbnailBackgroundImage.should.include('text..svg');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('The list has one downloaded file but it is deleted from the folder', () => {
|
||||||
|
const downloads = {
|
||||||
|
[file1.filename]: file1,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await env.createTestUserDataDirAsync();
|
||||||
|
await env.cleanTestConfigAsync();
|
||||||
|
await writeFileAsync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
|
||||||
|
await mkDirAsync(env.downloadsLocation);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.downloadsWindow = await env.openDownloadsDropdown(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rmDirAsync(env.downloadsLocation);
|
||||||
|
await this.app?.close?.();
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-22239 should display the file correctly (deleted)', async () => {
|
||||||
|
const filenameTextLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__Filename');
|
||||||
|
const filenameInnerText = await filenameTextLocator.innerText();
|
||||||
|
filenameInnerText.should.equal(downloads['file1.txt'].filename);
|
||||||
|
|
||||||
|
const fileStateLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__FileSizeAndStatus');
|
||||||
|
const fileStateInnerText = await fileStateLocator.innerText();
|
||||||
|
fileStateInnerText.should.equal('3.92 MB • Deleted');
|
||||||
|
|
||||||
|
const fileThumbnailLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__Thumbnail');
|
||||||
|
const thumbnailBackgroundImage = await fileThumbnailLocator.evaluate((node) => window.getComputedStyle(node).getPropertyValue('background-image'));
|
||||||
|
thumbnailBackgroundImage.should.include('text..svg');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('The list has one cancelled file', () => {
|
||||||
|
const downloads = {
|
||||||
|
[file1.filename]: {
|
||||||
|
...file1,
|
||||||
|
state: 'progressing',
|
||||||
|
progress: 50,
|
||||||
|
receivedBytes: 1958694,
|
||||||
|
totalBytes: 3917388,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await env.createTestUserDataDirAsync();
|
||||||
|
await env.cleanTestConfigAsync();
|
||||||
|
await writeFileAsync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
|
||||||
|
await mkDirAsync(env.downloadsLocation);
|
||||||
|
await writeFileAsync(path.join(env.downloadsLocation, 'file1.txt'), 'file1 content');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.downloadsWindow = await env.openDownloadsDropdown(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rmDirAsync(env.downloadsLocation);
|
||||||
|
await this.app?.close?.();
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-22239 should display the file correctly (cancelled)', async () => {
|
||||||
|
const filenameTextLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__Filename');
|
||||||
|
const filenameInnerText = await filenameTextLocator.innerText();
|
||||||
|
filenameInnerText.should.equal(downloads['file1.txt'].filename);
|
||||||
|
|
||||||
|
const fileStateLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__FileSizeAndStatus');
|
||||||
|
const fileStateInnerText = await fileStateLocator.innerText();
|
||||||
|
fileStateInnerText.should.equal('3.92 MB • Cancelled');
|
||||||
|
|
||||||
|
const fileThumbnailLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__Thumbnail');
|
||||||
|
const thumbnailBackgroundImage = await fileThumbnailLocator.evaluate((node) => window.getComputedStyle(node).getPropertyValue('background-image'));
|
||||||
|
thumbnailBackgroundImage.should.include('text..svg');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('The list has two downloaded files', () => {
|
||||||
|
const downloads = {
|
||||||
|
'file1.txt': file1,
|
||||||
|
'file2.txt': file2,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await env.createTestUserDataDirAsync();
|
||||||
|
await env.cleanTestConfigAsync();
|
||||||
|
await writeFileAsync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
|
||||||
|
await mkDirAsync(env.downloadsLocation);
|
||||||
|
await writeFileAsync(path.join(env.downloadsLocation, 'file1.txt'), 'file1 content');
|
||||||
|
await writeFileAsync(path.join(env.downloadsLocation, 'file2.txt'), 'file2 content');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.downloadsWindow = await env.openDownloadsDropdown(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rmDirAsync(env.downloadsLocation);
|
||||||
|
await this.app?.close?.();
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-22239 should display the files in correct order', async () => {
|
||||||
|
const filenameTextLocators = this.downloadsWindow.locator('.DownloadsDropdown__File__Body__Details__Filename');
|
||||||
|
(await filenameTextLocators.count()).should.equal(2);
|
||||||
|
const firstItemLocator = filenameTextLocators.first();
|
||||||
|
const file1InnerText = await firstItemLocator.innerText();
|
||||||
|
file1InnerText.should.equal(downloads['file2.txt'].filename); // newest first
|
||||||
|
const secondItemLocator = filenameTextLocators.nth(1);
|
||||||
|
const file2InnerText = await secondItemLocator.innerText();
|
||||||
|
file2InnerText.should.equal(downloads['file1.txt'].filename);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
81
e2e/specs/downloads/downloads_manager.test.js
Normal file
81
e2e/specs/downloads/downloads_manager.test.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep, rmDirAsync} = require('../../modules/utils');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
...env.demoMattermostConfig,
|
||||||
|
teams: [
|
||||||
|
...env.demoMattermostConfig.teams,
|
||||||
|
{
|
||||||
|
url: 'https://community.mattermost.com',
|
||||||
|
name: 'community',
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('downloads/downloads_manager', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
let firstServer;
|
||||||
|
const filename = `${Date.now().toString()}.txt`;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
|
||||||
|
const textbox = await firstServer.waitForSelector('#post_textbox');
|
||||||
|
const fileInput = await firstServer.waitForSelector('input[type="file"]');
|
||||||
|
await fileInput.setInputFiles({
|
||||||
|
name: filename,
|
||||||
|
mimeType: 'text/plain',
|
||||||
|
buffer: Buffer.from('this is test file'),
|
||||||
|
});
|
||||||
|
await asyncSleep(1000);
|
||||||
|
await textbox.focus();
|
||||||
|
robot.keyTap('enter');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rmDirAsync(env.downloadsLocation);
|
||||||
|
await this.app?.close?.();
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-22239 should open downloads dropdown when a download starts', async () => {
|
||||||
|
await firstServer.locator(`a[download="${filename}"]`).click();
|
||||||
|
await asyncSleep(1000);
|
||||||
|
(await env.downloadsDropdownIsOpen(this.app)).should.equal(true);
|
||||||
|
});
|
||||||
|
});
|
126
e2e/specs/downloads/downloads_menubar.test.js
Normal file
126
e2e/specs/downloads/downloads_menubar.test.js
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep, writeFileAsync} = require('../../modules/utils');
|
||||||
|
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
const downloads = {
|
||||||
|
'file1.txt': {
|
||||||
|
addedAt: Date.UTC(2022, 8, 8, 10), // Aug 08, 2022 10:00AM UTC
|
||||||
|
filename: 'file1.txt',
|
||||||
|
mimeType: 'plain/text',
|
||||||
|
location: path.join(env.downloadsLocation, 'file1.txt'),
|
||||||
|
progress: 100,
|
||||||
|
receivedBytes: 3917388,
|
||||||
|
state: 'completed',
|
||||||
|
totalBytes: 3917388,
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('downloads/downloads_menubar', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
describe('The download list is empty', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await env.createTestUserDataDirAsync();
|
||||||
|
await env.cleanTestConfigAsync();
|
||||||
|
await writeFileAsync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await writeFileAsync(env.downloadsFilePath, JSON.stringify({}));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await this.app?.close?.();
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-22239 should not show the downloads dropdown and the menu item should be disabled', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
await mainWindow.waitForLoadState();
|
||||||
|
await mainWindow.bringToFront();
|
||||||
|
|
||||||
|
const dlButton = mainWindow.locator('.DownloadsDropdownButton');
|
||||||
|
|
||||||
|
(await dlButton.isVisible()).should.equal(false);
|
||||||
|
|
||||||
|
const saveMenuItem = await this.app.evaluate(async ({app}) => {
|
||||||
|
const viewMenu = app.applicationMenu.getMenuItemById('view');
|
||||||
|
const saveItem = viewMenu.submenu.getMenuItemById('app-menu-downloads');
|
||||||
|
|
||||||
|
return saveItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
saveMenuItem.should.haveOwnProperty('enabled', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('The download list has one file', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await env.createTestUserDataDirAsync();
|
||||||
|
await env.cleanTestConfigAsync();
|
||||||
|
await writeFileAsync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await this.app?.close?.();
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-22239 should show the downloads dropdown button and the menu item should be enabled', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
await mainWindow.waitForLoadState();
|
||||||
|
await mainWindow.bringToFront();
|
||||||
|
|
||||||
|
const dlButton = await mainWindow.waitForSelector('.DownloadsDropdownButton', {state: 'attached'});
|
||||||
|
(await dlButton.isVisible()).should.equal(true);
|
||||||
|
|
||||||
|
const saveMenuItem = await this.app.evaluate(async ({app}) => {
|
||||||
|
const viewMenu = app.applicationMenu.getMenuItemById('view');
|
||||||
|
const saveItem = viewMenu.submenu.getMenuItemById('app-menu-downloads');
|
||||||
|
|
||||||
|
return saveItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
saveMenuItem.should.haveOwnProperty('enabled', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-22239 should open the downloads dropdown when clicking the download button in the menubar', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
await mainWindow.waitForLoadState();
|
||||||
|
await mainWindow.bringToFront();
|
||||||
|
|
||||||
|
const dlButton = await mainWindow.waitForSelector('.DownloadsDropdownButton', {state: 'attached'});
|
||||||
|
(await dlButton.isVisible()).should.equal(true);
|
||||||
|
await dlButton.click();
|
||||||
|
|
||||||
|
await asyncSleep(500);
|
||||||
|
(await env.downloadsDropdownIsOpen(this.app)).should.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-22239 should open the downloads dropdown from the app menu', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
await mainWindow.waitForLoadState();
|
||||||
|
await mainWindow.bringToFront();
|
||||||
|
|
||||||
|
await this.app.evaluate(async ({app}) => {
|
||||||
|
const viewMenu = app.applicationMenu.getMenuItemById('view');
|
||||||
|
const downloadsItem = viewMenu.submenu.getMenuItemById('app-menu-downloads');
|
||||||
|
|
||||||
|
downloadsItem.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await asyncSleep(500);
|
||||||
|
(await env.downloadsDropdownIsOpen(this.app)).should.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
162
e2e/specs/focus.test.js
Normal file
162
e2e/specs/focus.test.js
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
const {SHOW_SETTINGS_WINDOW} = require('src/common/communication');
|
||||||
|
|
||||||
|
const env = require('../modules/environment');
|
||||||
|
const {asyncSleep} = require('../modules/utils');
|
||||||
|
|
||||||
|
describe('focus', function desc() {
|
||||||
|
this.timeout(40000);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
...env.demoMattermostConfig,
|
||||||
|
teams: [
|
||||||
|
...env.demoMattermostConfig.teams,
|
||||||
|
{
|
||||||
|
name: 'community',
|
||||||
|
url: 'https://community.mattermost.com',
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let firstServer;
|
||||||
|
let loadingScreen;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
|
||||||
|
loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
const textbox = await firstServer.waitForSelector('#post_textbox');
|
||||||
|
textbox.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Focus textbox tests', () => {
|
||||||
|
it('MM-T1315 should return focus to the message box when closing the settings window', async () => {
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
await settingsWindow.close();
|
||||||
|
|
||||||
|
const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement);
|
||||||
|
isTextboxFocused.should.be.true;
|
||||||
|
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
|
||||||
|
// Make sure you can just start typing and it'll go in the post textbox
|
||||||
|
await asyncSleep(500);
|
||||||
|
await firstServer.fill('#post_textbox', 'Mattermost');
|
||||||
|
await asyncSleep(500);
|
||||||
|
|
||||||
|
const textboxString = await firstServer.inputValue('#post_textbox');
|
||||||
|
textboxString.should.equal('Mattermost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T1316 should return focus to the message box when closing the settings window', async () => {
|
||||||
|
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
await mainView.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.click('.ServerDropdown .ServerDropdown__button.addServer');
|
||||||
|
const newServerView = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('newServer'),
|
||||||
|
});
|
||||||
|
await newServerView.waitForSelector('#cancelNewServerModal');
|
||||||
|
await newServerView.click('#cancelNewServerModal');
|
||||||
|
|
||||||
|
const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement);
|
||||||
|
isTextboxFocused.should.be.true;
|
||||||
|
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
|
||||||
|
// Make sure you can just start typing and it'll go in the post textbox
|
||||||
|
await asyncSleep(500);
|
||||||
|
await firstServer.fill('#post_textbox', 'Mattermost');
|
||||||
|
await asyncSleep(500);
|
||||||
|
|
||||||
|
const textboxString = await firstServer.inputValue('#post_textbox');
|
||||||
|
textboxString.should.equal('Mattermost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T1317 should return focus to the focused box when switching servers', async () => {
|
||||||
|
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
await mainView.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.click('.ServerDropdown .ServerDropdown__button:has-text("community")');
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
const secondServer = this.serverMap['community___TAB_MESSAGING'].win;
|
||||||
|
await secondServer.waitForSelector('#input_loginId');
|
||||||
|
await secondServer.focus('#input_loginId');
|
||||||
|
|
||||||
|
await mainView.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.click(`.ServerDropdown .ServerDropdown__button:has-text("${config.teams[0].name}")`);
|
||||||
|
const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement);
|
||||||
|
isTextboxFocused.should.be.true;
|
||||||
|
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
|
||||||
|
// Make sure you can just start typing and it'll go in the post textbox
|
||||||
|
await asyncSleep(500);
|
||||||
|
await firstServer.fill('#post_textbox', 'Mattermost');
|
||||||
|
await asyncSleep(500);
|
||||||
|
|
||||||
|
const textboxString = await firstServer.inputValue('#post_textbox');
|
||||||
|
textboxString.should.equal('Mattermost');
|
||||||
|
|
||||||
|
await mainView.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.click('.ServerDropdown .ServerDropdown__button:has-text("community")');
|
||||||
|
const isLoginFocused = await secondServer.$eval('#input_loginId', (el) => el === document.activeElement);
|
||||||
|
isLoginFocused.should.be.true;
|
||||||
|
|
||||||
|
// Make sure you can just start typing and it'll go in the post textbox
|
||||||
|
await asyncSleep(500);
|
||||||
|
robot.typeString('username');
|
||||||
|
await asyncSleep(500);
|
||||||
|
|
||||||
|
const loginString = await secondServer.inputValue('#input_loginId');
|
||||||
|
loginString.should.equal('username');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
66
e2e/specs/linux_dark_mode.test.js
Normal file
66
e2e/specs/linux_dark_mode.test.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../modules/environment');
|
||||||
|
const {asyncSleep} = require('../modules/utils');
|
||||||
|
|
||||||
|
describe('dark_mode', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
it('MM-T2465 Linux Dark Mode Toggle', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
mainWindow.should.not.be.null;
|
||||||
|
|
||||||
|
// Toggle Dark Mode
|
||||||
|
await toggleDarkMode();
|
||||||
|
|
||||||
|
const topBarElementWithDarkMode = await mainWindow.waitForSelector('.topBar');
|
||||||
|
const topBarElementClassWithDarkMode = await topBarElementWithDarkMode.getAttribute('class');
|
||||||
|
|
||||||
|
topBarElementClassWithDarkMode.should.contain('topBar darkMode row');
|
||||||
|
|
||||||
|
// Toggle Light Mode
|
||||||
|
await toggleDarkMode();
|
||||||
|
|
||||||
|
const topBarElementWithLightMode = await mainWindow.waitForSelector('.topBar');
|
||||||
|
const topBarElementClassWithLightMode = await topBarElementWithLightMode.getAttribute('class');
|
||||||
|
|
||||||
|
topBarElementClassWithLightMode.should.contain('topBar row');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function toggleDarkMode() {
|
||||||
|
robot.keyTap('alt');
|
||||||
|
robot.keyTap('enter');
|
||||||
|
robot.keyTap('v');
|
||||||
|
robot.keyTap('t');
|
||||||
|
await asyncSleep(500); // Add a sleep because sometimes the second 't' doesn't fire
|
||||||
|
robot.keyTap('t'); // Click on "Toggle Dark Mode" menu item
|
||||||
|
robot.keyTap('enter');
|
||||||
|
}
|
60
e2e/specs/mattermost/copy_link.test.js
Normal file
60
e2e/specs/mattermost/copy_link.test.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const {clipboard} = require('electron');
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('copylink', function desc() {
|
||||||
|
this.timeout(40000);
|
||||||
|
|
||||||
|
const config = env.demoMattermostConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T125 Copy Link can be used from channel LHS', async () => {
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#sidebarItem_town-square');
|
||||||
|
await firstServer.click('#sidebarItem_town-square', {button: 'right'});
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'linux':
|
||||||
|
case 'win32':
|
||||||
|
robot.keyTap('down');
|
||||||
|
robot.keyTap('down');
|
||||||
|
break;
|
||||||
|
case 'darwin':
|
||||||
|
robot.keyTap('c');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
robot.keyTap('enter');
|
||||||
|
await firstServer.click('#sidebarItem_town-square');
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
const clipboardText = clipboard.readText();
|
||||||
|
await firstServer.fill('#post_textbox', clipboardText);
|
||||||
|
const content = await firstServer.locator('#post_textbox').textContent();
|
||||||
|
content.should.contain('/ad-1/channels/town-square');
|
||||||
|
});
|
||||||
|
});
|
119
e2e/specs/menu_bar/dropdown.test.js
Normal file
119
e2e/specs/menu_bar/dropdown.test.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
describe('menu_bar/dropdown', function desc() {
|
||||||
|
const beforeFunc = async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
const afterFunc = async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
it('MM-T4405 should set name of menu item from config file', async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
await mainWindow.click('.ServerDropdownButton');
|
||||||
|
const firstMenuItem = await dropdownView.innerText('.ServerDropdown button.ServerDropdown__button:nth-child(1) span');
|
||||||
|
const secondMenuItem = await dropdownView.innerText('.ServerDropdown button.ServerDropdown__button:nth-child(2) span');
|
||||||
|
|
||||||
|
firstMenuItem.should.equal(config.teams[0].name);
|
||||||
|
secondMenuItem.should.equal(config.teams[1].name);
|
||||||
|
|
||||||
|
await afterFunc();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MM-T4406 should only show dropdown when button is clicked', async () => {
|
||||||
|
let mainWindow;
|
||||||
|
let browserWindow;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
});
|
||||||
|
after(afterFunc);
|
||||||
|
|
||||||
|
it('MM-T4406_1 should show the dropdown', async () => {
|
||||||
|
let dropdownHeight = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
|
||||||
|
dropdownHeight.should.equal(0);
|
||||||
|
|
||||||
|
await mainWindow.click('.ServerDropdownButton');
|
||||||
|
dropdownHeight = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
|
||||||
|
dropdownHeight.should.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4406_2 should hide the dropdown', async () => {
|
||||||
|
await mainWindow.click('.TabBar');
|
||||||
|
const dropdownHeight = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
|
||||||
|
dropdownHeight.should.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4407 should open the new server prompt after clicking the add button', async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
await mainWindow.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.click('.ServerDropdown__button.addServer');
|
||||||
|
|
||||||
|
const newServerModal = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('newServer'),
|
||||||
|
});
|
||||||
|
const modalTitle = await newServerModal.innerText('#newServerModal .modal-title');
|
||||||
|
modalTitle.should.equal('Add Server');
|
||||||
|
|
||||||
|
await afterFunc();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MM-T4408 Switch Servers', async () => {
|
||||||
|
let mainWindow;
|
||||||
|
let browserWindow;
|
||||||
|
let dropdownView;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
});
|
||||||
|
after(afterFunc);
|
||||||
|
|
||||||
|
it('MM-T4408_1 should show the first view', async () => {
|
||||||
|
const firstViewIsAttached = await browserWindow.evaluate((window, url) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === url)), env.exampleURL);
|
||||||
|
firstViewIsAttached.should.be.true;
|
||||||
|
const secondViewIsAttached = await browserWindow.evaluate((window) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === 'https://github.com/')));
|
||||||
|
secondViewIsAttached.should.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4408_2 should show the second view after clicking the menu item', async () => {
|
||||||
|
await mainWindow.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.click('.ServerDropdown button.ServerDropdown__button:nth-child(2)');
|
||||||
|
|
||||||
|
const firstViewIsAttached = await browserWindow.evaluate((window, url) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === url)), env.exampleURL);
|
||||||
|
firstViewIsAttached.should.be.false;
|
||||||
|
const secondViewIsAttached = await browserWindow.evaluate((window) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === 'https://github.com/')));
|
||||||
|
secondViewIsAttached.should.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
125
e2e/specs/menu_bar/edit_menu.test.js
Normal file
125
e2e/specs/menu_bar/edit_menu.test.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('edit_menu', function desc() {
|
||||||
|
this.timeout(40000);
|
||||||
|
|
||||||
|
const config = env.demoMattermostConfig;
|
||||||
|
let firstServer;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T807 Undo in the Menu Bar', async () => {
|
||||||
|
// click on sint channel
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
await firstServer.type('#post_textbox', 'Mattermost');
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
robot.keyTap('z', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const content = await firstServer.inputValue('#post_textbox');
|
||||||
|
content.should.be.equal('Mattermos');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T808 Redo in the Menu Bar', async () => {
|
||||||
|
// click on sint channel
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
await firstServer.type('#post_textbox', 'Mattermost');
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
robot.keyTap('z', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const textAfterUndo = await firstServer.inputValue('#post_textbox');
|
||||||
|
textAfterUndo.should.be.equal('Mattermos');
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
robot.keyTap('z', ['shift', env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const content = await firstServer.inputValue('#post_textbox');
|
||||||
|
content.should.be.equal('Mattermost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T809 Cut in the Menu Bar', async () => {
|
||||||
|
// click on sint channel
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
await firstServer.type('#post_textbox', 'Mattermost');
|
||||||
|
robot.keyTap('a', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
robot.keyTap('x', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const content = await firstServer.inputValue('#post_textbox');
|
||||||
|
content.should.be.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T810 Copy in the Menu Bar', async () => {
|
||||||
|
// click on sint channel
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
await firstServer.type('#post_textbox', 'Mattermost');
|
||||||
|
robot.keyTap('a', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
robot.keyTap('c', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
robot.keyTap('v', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const content = await firstServer.inputValue('#post_textbox');
|
||||||
|
content.should.be.equal('MattermostMattermost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T811 Paste in the Menu Bar', async () => {
|
||||||
|
// click on sint channel
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
await firstServer.type('#post_textbox', 'Mattermost');
|
||||||
|
robot.keyTap('a', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
robot.keyTap('c', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
robot.keyTap('a', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
robot.keyTap('v', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const content = await firstServer.inputValue('#post_textbox');
|
||||||
|
content.should.be.equal('Mattermost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T812 Select All in the Menu Bar', async () => {
|
||||||
|
// click on sint channel
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
await firstServer.fill('#post_textbox', 'Mattermost');
|
||||||
|
robot.keyTap('a', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const channelHeaderText = await firstServer.evaluate('window.getSelection().toString()');
|
||||||
|
channelHeaderText.should.equal('Mattermost');
|
||||||
|
});
|
||||||
|
});
|
99
e2e/specs/menu_bar/file_menu.test.js
Normal file
99
e2e/specs/menu_bar/file_menu.test.js
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('file_menu/dropdown', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
let skipAfterEach = false;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
|
||||||
|
skipAfterEach = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app && skipAfterEach === false) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T1313 Open Settings modal using keyboard shortcuts', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
mainWindow.should.not.be.null;
|
||||||
|
robot.keyTap(',', [env.cmdOrCtrl]);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
settingsWindow.should.not.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: No keyboard shortcut for macOS
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
it('MM-T805 Sign in to Another Server Window opens using menu item', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
mainWindow.should.not.be.null;
|
||||||
|
await mainWindow.click('button.three-dot-menu');
|
||||||
|
robot.keyTap('f');
|
||||||
|
robot.keyTap('s');
|
||||||
|
robot.keyTap('s');
|
||||||
|
robot.keyTap('enter');
|
||||||
|
const signInToAnotherServerWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('newServer'),
|
||||||
|
});
|
||||||
|
signInToAnotherServerWindow.should.not.be.null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
it('MM-T804 Preferences in Menu Bar open the Settings page', async () => {
|
||||||
|
//Opening the menu bar
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
mainWindow.should.not.be.null;
|
||||||
|
await mainWindow.click('button.three-dot-menu');
|
||||||
|
robot.keyTap('f');
|
||||||
|
robot.keyTap('s');
|
||||||
|
robot.keyTap('enter');
|
||||||
|
const settingsWindowFromMenu = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
settingsWindowFromMenu.should.not.be.null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Causes issues on Windows so skipping for Windows
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
it('MM-T806 Exit in the Menu Bar', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
mainWindow.should.not.be.null;
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
robot.keyTap('q', ['command']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||||
|
robot.keyTap('q', ['control']);
|
||||||
|
}
|
||||||
|
|
||||||
|
await asyncSleep(500);
|
||||||
|
this.app.windows().find((window) => window.url().should.not.include('index'));
|
||||||
|
|
||||||
|
skipAfterEach = true; // Need to skip closing in aftereach as apps execution context is destroyed above
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
66
e2e/specs/menu_bar/full_screen.test.js
Normal file
66
e2e/specs/menu_bar/full_screen.test.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('menu/view', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
const config = env.demoMattermostConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
fs.writeFileSync(env.boundsInfoPath, JSON.stringify({x: 0, y: 0, width: 600, height: 240, maximized: false, fullscreen: false}));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: No keyboard shortcut for macOS
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
it('MM-T816 Toggle Full Screen in the Menu Bar', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#post_textbox');
|
||||||
|
let currentWidth = await firstServer.evaluate('window.outerWidth');
|
||||||
|
let currentHeight = await firstServer.evaluate('window.outerHeight');
|
||||||
|
await mainWindow.click('button.three-dot-menu');
|
||||||
|
robot.keyTap('v');
|
||||||
|
robot.keyTap('t');
|
||||||
|
robot.keyTap('enter');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const fullScreenWidth = await firstServer.evaluate('window.outerWidth');
|
||||||
|
const fullScreenHeight = await firstServer.evaluate('window.outerHeight');
|
||||||
|
fullScreenWidth.should.be.greaterThan(currentWidth);
|
||||||
|
fullScreenHeight.should.be.greaterThan(currentHeight);
|
||||||
|
await mainWindow.click('button.three-dot-menu');
|
||||||
|
robot.keyTap('v');
|
||||||
|
robot.keyTap('t');
|
||||||
|
robot.keyTap('enter');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
currentWidth = await firstServer.evaluate('window.outerWidth');
|
||||||
|
currentHeight = await firstServer.evaluate('window.outerHeight');
|
||||||
|
currentWidth.should.be.lessThan(fullScreenWidth);
|
||||||
|
currentHeight.should.be.lessThan(fullScreenHeight);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
51
e2e/specs/menu_bar/history_menu.test.js
Normal file
51
e2e/specs/menu_bar/history_menu.test.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('history_menu', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
const config = env.demoMattermostConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Click back and forward from history', async () => {
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#sidebarItem_off-topic');
|
||||||
|
|
||||||
|
// click on Off topic channel
|
||||||
|
await firstServer.click('#sidebarItem_off-topic');
|
||||||
|
|
||||||
|
// click on town square channel
|
||||||
|
await firstServer.click('#sidebarItem_town-square');
|
||||||
|
await firstServer.locator('[aria-label="Back"]').click();
|
||||||
|
let channelHeaderText = await firstServer.$eval('#channelHeaderTitle', (el) => el.firstChild.innerHTML);
|
||||||
|
channelHeaderText.should.equal('Off-Topic');
|
||||||
|
await firstServer.locator('[aria-label="Forward"]').click();
|
||||||
|
channelHeaderText = await firstServer.$eval('#channelHeaderTitle', (el) => el.firstChild.innerHTML);
|
||||||
|
channelHeaderText.should.equal('Town Square');
|
||||||
|
});
|
||||||
|
});
|
53
e2e/specs/menu_bar/menu.test.js
Normal file
53
e2e/specs/menu_bar/menu.test.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('menu/menu', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
it('MM-T4404 should open the 3 dot menu with Alt', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
mainWindow.should.not.be.null;
|
||||||
|
|
||||||
|
await mainWindow.bringToFront();
|
||||||
|
await mainWindow.click('#app');
|
||||||
|
|
||||||
|
// Settings window should open if Alt works
|
||||||
|
robot.keyTap('alt');
|
||||||
|
robot.keyTap('enter');
|
||||||
|
robot.keyTap('f');
|
||||||
|
robot.keyTap('s');
|
||||||
|
robot.keyTap('enter');
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
settingsWindow.should.not.be.null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
233
e2e/specs/menu_bar/view_menu.test.js
Normal file
233
e2e/specs/menu_bar/view_menu.test.js
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
async function setupPromise(window, id) {
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
const browserView = window.getBrowserViews().find((view) => view.webContents.id === id);
|
||||||
|
browserView.webContents.on('did-finish-load', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getZoomFactorOfServer(browserWindow, serverId) {
|
||||||
|
return browserWindow.evaluate(
|
||||||
|
(window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(),
|
||||||
|
serverId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function setZoomFactorOfServer(browserWindow, serverId, zoomFactor) {
|
||||||
|
return browserWindow.evaluate(
|
||||||
|
(window, {id, zoom}) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.setZoomFactor(zoom),
|
||||||
|
{id: serverId, zoom: zoomFactor},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('menu/view', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
const config = env.demoMattermostConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T813 Control+F should focus the search bar in Mattermost', async () => {
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#searchBox');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
robot.keyTap('f', [process.platform === 'darwin' ? 'command' : 'control']);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const isFocused = await firstServer.$eval('#searchBox', (el) => el === document.activeElement);
|
||||||
|
isFocused.should.be.true;
|
||||||
|
const text = await firstServer.inputValue('#searchBox');
|
||||||
|
text.should.include('in:');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T817 Actual Size Zoom in the menu bar', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
const firstServerId = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].webContentsId;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#searchBox');
|
||||||
|
|
||||||
|
robot.keyTap('=', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
let zoomLevel = await browserWindow.evaluate((window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
|
||||||
|
zoomLevel.should.be.greaterThan(1);
|
||||||
|
|
||||||
|
robot.keyTap('0', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
zoomLevel = await browserWindow.evaluate((window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
|
||||||
|
zoomLevel.should.be.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MM-T818 Zoom in from the menu bar', () => {
|
||||||
|
it('MM-T818_1 Zoom in when CmdOrCtrl+Plus is pressed', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
const firstServerId = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].webContentsId;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#searchBox');
|
||||||
|
|
||||||
|
robot.keyTap('=', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const zoomLevel = await browserWindow.evaluate((window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
|
||||||
|
zoomLevel.should.be.greaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T818_2 Zoom in when CmdOrCtrl+Shift+Plus is pressed', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
const firstServerId = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].webContentsId;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#searchBox');
|
||||||
|
|
||||||
|
// reset zoom
|
||||||
|
await setZoomFactorOfServer(browserWindow, firstServerId, 1);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const initialZoom = await getZoomFactorOfServer(browserWindow, firstServerId);
|
||||||
|
initialZoom.should.be.equal(1);
|
||||||
|
|
||||||
|
robot.keyTap('=', [env.cmdOrCtrl, 'shift']);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const zoomLevel = await getZoomFactorOfServer(browserWindow, firstServerId);
|
||||||
|
zoomLevel.should.be.greaterThan(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MM-T819 Zoom out from the menu bar', () => {
|
||||||
|
it('MM-T819_1 Zoom out when CmdOrCtrl+Minus is pressed', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
const firstServerId = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].webContentsId;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#searchBox');
|
||||||
|
|
||||||
|
robot.keyTap('-', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const zoomLevel = await browserWindow.evaluate((window, id) => window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getZoomFactor(), firstServerId);
|
||||||
|
zoomLevel.should.be.lessThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T819_2 Zoom out when CmdOrCtrl+Shift+Minus is pressed', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
const firstServerId = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].webContentsId;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#searchBox');
|
||||||
|
|
||||||
|
// reset zoom
|
||||||
|
await setZoomFactorOfServer(browserWindow, firstServerId, 1.0);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const initialZoom = await getZoomFactorOfServer(browserWindow, firstServerId);
|
||||||
|
initialZoom.should.be.equal(1);
|
||||||
|
|
||||||
|
robot.keyTap('-', [env.cmdOrCtrl, 'shift']);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const zoomLevel = await getZoomFactorOfServer(browserWindow, firstServerId);
|
||||||
|
zoomLevel.should.be.lessThan(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Reload', () => {
|
||||||
|
let browserWindow;
|
||||||
|
let webContentsId;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
webContentsId = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].webContentsId;
|
||||||
|
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T814 should reload page when pressing Ctrl+R', async () => {
|
||||||
|
const check = browserWindow.evaluate(setupPromise, webContentsId);
|
||||||
|
await asyncSleep(500);
|
||||||
|
robot.keyTap('r', [env.cmdOrCtrl]);
|
||||||
|
const result = await check;
|
||||||
|
result.should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T815 should reload page when pressing Ctrl+Shift+R', async () => {
|
||||||
|
const check = browserWindow.evaluate(setupPromise, webContentsId);
|
||||||
|
await asyncSleep(500);
|
||||||
|
robot.keyTap('r', [env.cmdOrCtrl, 'shift']);
|
||||||
|
const result = await check;
|
||||||
|
result.should.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T820 should open Developer Tools For Application Wrapper for main window', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index.html'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
|
||||||
|
let isDevToolsOpen = await browserWindow.evaluate((window) => {
|
||||||
|
return window.webContents.isDevToolsOpened();
|
||||||
|
});
|
||||||
|
isDevToolsOpen.should.be.false;
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
// Press Command + Option + I
|
||||||
|
robot.keyTap('i', ['command', 'alt']);
|
||||||
|
await asyncSleep(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
robot.keyToggle('shift', 'down');
|
||||||
|
robot.keyToggle('control', 'down');
|
||||||
|
robot.keyTap('i');
|
||||||
|
}
|
||||||
|
|
||||||
|
await asyncSleep(1000);
|
||||||
|
isDevToolsOpen = await browserWindow.evaluate((window) => {
|
||||||
|
return window.webContents.isDevToolsOpened();
|
||||||
|
});
|
||||||
|
isDevToolsOpen.should.be.true;
|
||||||
|
});
|
||||||
|
});
|
185
e2e/specs/menu_bar/window_menu.test.js
Normal file
185
e2e/specs/menu_bar/window_menu.test.js
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('Menu/window_menu', function desc() {
|
||||||
|
const config = {
|
||||||
|
...env.demoConfig,
|
||||||
|
teams: [
|
||||||
|
...env.demoConfig.teams,
|
||||||
|
{
|
||||||
|
name: 'google',
|
||||||
|
url: 'https://google.com/',
|
||||||
|
order: 2,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTeam: 2,
|
||||||
|
minimizeToTray: true,
|
||||||
|
alwaysMinimize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeFunc = async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
const afterFunc = async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
describe('MM-T826 should switch to servers when keyboard shortcuts are pressed', async () => {
|
||||||
|
let mainWindow;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
await env.getServerMap(this.app);
|
||||||
|
mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
});
|
||||||
|
after(afterFunc);
|
||||||
|
|
||||||
|
it('MM-T826_1 should show the second server', async () => {
|
||||||
|
let dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton');
|
||||||
|
dropdownButtonText.should.equal('google');
|
||||||
|
|
||||||
|
robot.keyTap('2', ['control', process.platform === 'darwin' ? 'command' : 'shift']);
|
||||||
|
dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton:has-text("github")');
|
||||||
|
dropdownButtonText.should.equal('github');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T826_2 should show the third server', async () => {
|
||||||
|
robot.keyTap('3', ['control', process.platform === 'darwin' ? 'command' : 'shift']);
|
||||||
|
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton:has-text("google")');
|
||||||
|
dropdownButtonText.should.equal('google');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T826_3 should show the first server', async () => {
|
||||||
|
robot.keyTap('1', ['control', process.platform === 'darwin' ? 'command' : 'shift']);
|
||||||
|
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton:has-text("example")');
|
||||||
|
dropdownButtonText.should.equal('example');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MM-T4385 select tab from menu', async () => {
|
||||||
|
let mainView;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
});
|
||||||
|
after(afterFunc);
|
||||||
|
|
||||||
|
it('MM-T4385_1 should show the second tab', async () => {
|
||||||
|
let tabViewButton = await mainView.innerText('.active');
|
||||||
|
tabViewButton.should.equal('Channels');
|
||||||
|
|
||||||
|
robot.keyTap('2', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
tabViewButton = await mainView.innerText('.active');
|
||||||
|
tabViewButton.should.equal('Boards');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4385_2 should show the third tab', async () => {
|
||||||
|
robot.keyTap('3', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const tabViewButton = await mainView.innerText('.active');
|
||||||
|
tabViewButton.should.equal('Playbooks');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4385_3 should show the first tab', async () => {
|
||||||
|
robot.keyTap('1', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(500);
|
||||||
|
const tabViewButton = await mainView.innerText('.active');
|
||||||
|
tabViewButton.should.equal('Channels');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T827 select next/previous tab', async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
|
||||||
|
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
|
||||||
|
let tabViewButton = await mainView.innerText('.active');
|
||||||
|
tabViewButton.should.equal('Channels');
|
||||||
|
|
||||||
|
robot.keyTap('tab', ['control']);
|
||||||
|
await asyncSleep(500);
|
||||||
|
tabViewButton = await mainView.innerText('.active');
|
||||||
|
tabViewButton.should.equal('Boards');
|
||||||
|
|
||||||
|
robot.keyTap('tab', ['shift', 'control']);
|
||||||
|
await asyncSleep(500);
|
||||||
|
tabViewButton = await mainView.innerText('.active');
|
||||||
|
tabViewButton.should.equal('Channels');
|
||||||
|
|
||||||
|
await afterFunc();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T824 should be minimized when keyboard shortcuts are pressed', async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
robot.keyTap('m', [env.cmdOrCtrl]);
|
||||||
|
} else {
|
||||||
|
await mainWindow.click('button.three-dot-menu');
|
||||||
|
robot.keyTap('w');
|
||||||
|
robot.keyTap('m');
|
||||||
|
robot.keyTap('enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
await asyncSleep(2000);
|
||||||
|
const isMinimized = await browserWindow.evaluate((window) => window.isMinimized());
|
||||||
|
isMinimized.should.be.true;
|
||||||
|
|
||||||
|
await afterFunc();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T825 should be hidden when keyboard shortcuts are pressed', async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
robot.keyTap('w', [env.cmdOrCtrl]);
|
||||||
|
await asyncSleep(2000);
|
||||||
|
const isVisible = await browserWindow.evaluate((window) => window.isVisible());
|
||||||
|
isVisible.should.be.false;
|
||||||
|
const isDestroyed = await browserWindow.evaluate((window) => window.isDestroyed());
|
||||||
|
isDestroyed.should.be.false;
|
||||||
|
|
||||||
|
await afterFunc();
|
||||||
|
});
|
||||||
|
});
|
116
e2e/specs/popup.test.js
Normal file
116
e2e/specs/popup.test.js
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
|
||||||
|
const env = require('../modules/environment');
|
||||||
|
const {asyncSleep} = require('../modules/utils');
|
||||||
|
|
||||||
|
describe('popup', function desc() {
|
||||||
|
this.timeout(40000);
|
||||||
|
|
||||||
|
const config = env.demoMattermostConfig;
|
||||||
|
let popupWindow;
|
||||||
|
let firstServer;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
await firstServer.fill('#post_textbox', '');
|
||||||
|
await firstServer.type('#post_textbox', '/github connect ');
|
||||||
|
await firstServer.click('button[data-testid="SendMessageButton"]');
|
||||||
|
|
||||||
|
const githubLink = await firstServer.waitForSelector('a.theme.markdown__link:has-text("GitHub account")');
|
||||||
|
githubLink.click();
|
||||||
|
popupWindow = await this.app.waitForEvent('window');
|
||||||
|
|
||||||
|
const loginField = await popupWindow.waitForSelector('#login_field');
|
||||||
|
await loginField.focus();
|
||||||
|
robot.typeString('Mattermost');
|
||||||
|
await asyncSleep(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: These tests requires that the test server have the GitHub plugin configured
|
||||||
|
it('MM-T2827_1 should be able to select all in popup windows', async () => {
|
||||||
|
robot.keyTap('a', env.cmdOrCtrl);
|
||||||
|
await asyncSleep(1000);
|
||||||
|
|
||||||
|
const selectedText = await popupWindow.evaluate(() => {
|
||||||
|
const box = document.querySelectorAll('#login_field')[0];
|
||||||
|
return box.value.substring(box.selectionStart,
|
||||||
|
box.selectionEnd);
|
||||||
|
});
|
||||||
|
await asyncSleep(3000);
|
||||||
|
selectedText.should.equal('Mattermost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2827_2 should be able to cut and paste in popup windows', async () => {
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const textbox = await popupWindow.waitForSelector('#login_field');
|
||||||
|
|
||||||
|
await textbox.selectText({force: true});
|
||||||
|
robot.keyTap('x', env.cmdOrCtrl);
|
||||||
|
let textValue = await textbox.inputValue();
|
||||||
|
textValue.should.equal('');
|
||||||
|
|
||||||
|
await textbox.focus();
|
||||||
|
robot.keyTap('v', env.cmdOrCtrl);
|
||||||
|
textValue = await textbox.inputValue();
|
||||||
|
textValue.should.equal('Mattermost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2827_3 should be able to copy and paste in popup windows', async () => {
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const textbox = await popupWindow.waitForSelector('#login_field');
|
||||||
|
|
||||||
|
await textbox.selectText({force: true});
|
||||||
|
robot.keyTap('c', env.cmdOrCtrl);
|
||||||
|
await textbox.focus();
|
||||||
|
await textbox.type('other-text');
|
||||||
|
robot.keyTap('v', env.cmdOrCtrl);
|
||||||
|
const textValue = await textbox.inputValue();
|
||||||
|
textValue.should.equal('other-textMattermost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T1659 should not be able to go Back or Forward in the popup window', async () => {
|
||||||
|
const currentURL = popupWindow.url();
|
||||||
|
|
||||||
|
// Try and go back
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
robot.keyTap('[', ['command']);
|
||||||
|
} else {
|
||||||
|
robot.keyTap('left', ['alt']);
|
||||||
|
}
|
||||||
|
popupWindow.url().should.equal(currentURL);
|
||||||
|
|
||||||
|
// Try and go forward
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
robot.keyTap(']', ['command']);
|
||||||
|
} else {
|
||||||
|
robot.keyTap('right', ['alt']);
|
||||||
|
}
|
||||||
|
popupWindow.url().should.equal(currentURL);
|
||||||
|
});
|
||||||
|
});
|
46
e2e/specs/relative_url/relative_url.test.js
Normal file
46
e2e/specs/relative_url/relative_url.test.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const {expect} = require('chai');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('copylink', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
const config = env.demoMattermostConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.cleanDataDir();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
this.serverMap = await env.getServerMap(this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T1308 Check that external links dont open in the app', async () => {
|
||||||
|
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
|
||||||
|
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
|
||||||
|
const firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
|
||||||
|
await env.loginToMattermost(firstServer);
|
||||||
|
await firstServer.waitForSelector('#post_textbox');
|
||||||
|
await firstServer.click('#post_textbox');
|
||||||
|
await firstServer.fill('#post_textbox', 'https://electronjs.org/apps/mattermost');
|
||||||
|
await firstServer.press('#post_textbox', 'Enter');
|
||||||
|
const newPageWindow = this.app.windows().find((window) => window.url().includes('apps/mattermost'));
|
||||||
|
expect(newPageWindow === undefined);
|
||||||
|
});
|
||||||
|
});
|
150
e2e/specs/server_management/add_server_modal.test.js
Normal file
150
e2e/specs/server_management/add_server_modal.test.js
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('Add Server Modal', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
|
||||||
|
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
await mainView.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.click('.ServerDropdown .ServerDropdown__button.addServer');
|
||||||
|
newServerView = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('newServer'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for autofocus to finish
|
||||||
|
await asyncSleep(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
let newServerView;
|
||||||
|
|
||||||
|
it('MM-T1312 should focus the first text input', async () => {
|
||||||
|
const isFocused = await newServerView.$eval('#serverUrlInput', (el) => el.isSameNode(document.activeElement));
|
||||||
|
isFocused.should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4388 should close the window after clicking cancel', async () => {
|
||||||
|
await newServerView.click('#cancelNewServerModal');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('newServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MM-T4389 Invalid messages', () => {
|
||||||
|
it('MM-T4389_1 should not be valid and save should be disabled if no server name or URL has been set', async () => {
|
||||||
|
const existing = await newServerView.isVisible('#nameValidation.error');
|
||||||
|
existing.should.be.true;
|
||||||
|
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
||||||
|
(disabled === '').should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should warn the user if a server with the same URL exists, but still allow them to save', async () => {
|
||||||
|
await newServerView.type('#serverNameInput', 'some-new-server');
|
||||||
|
await newServerView.type('#serverUrlInput', config.teams[0].url);
|
||||||
|
await newServerView.waitForSelector('#urlValidation.warning');
|
||||||
|
const existing = await newServerView.isVisible('#urlValidation.warning');
|
||||||
|
existing.should.be.true;
|
||||||
|
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
||||||
|
(disabled === '').should.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Valid server name', async () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await newServerView.type('#serverNameInput', 'TestServer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4389_2 Name should not be marked invalid, but should not be able to save', async () => {
|
||||||
|
await newServerView.waitForSelector('#nameValidation.error', {state: 'detached'});
|
||||||
|
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
||||||
|
(disabled === '').should.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Valid server url', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await newServerView.type('#serverUrlInput', 'http://example.org');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4389_3 URL should not be marked invalid, name should be marked invalid', async () => {
|
||||||
|
const existingUrl = await newServerView.isVisible('#urlValidation.error');
|
||||||
|
const existingName = await newServerView.isVisible('#nameValidation.error');
|
||||||
|
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
||||||
|
existingName.should.be.true;
|
||||||
|
existingUrl.should.be.false;
|
||||||
|
(disabled === '').should.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2826_1 should not be valid if an invalid server address has been set', async () => {
|
||||||
|
await newServerView.type('#serverUrlInput', 'superInvalid url');
|
||||||
|
await newServerView.waitForSelector('#urlValidation.error');
|
||||||
|
const existing = await newServerView.isVisible('#urlValidation.error');
|
||||||
|
existing.should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Valid Team Settings', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await newServerView.type('#serverUrlInput', 'http://example.org');
|
||||||
|
await newServerView.type('#serverNameInput', 'TestServer');
|
||||||
|
await newServerView.waitForSelector('#urlValidation.warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to click add', async () => {
|
||||||
|
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
|
||||||
|
(disabled === null).should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2826_2 should add the server to the config file', async () => {
|
||||||
|
await newServerView.click('#saveNewServerModal');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('newServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||||
|
savedConfig.teams.should.deep.contain({
|
||||||
|
name: 'TestServer',
|
||||||
|
url: 'http://example.org/',
|
||||||
|
order: 2,
|
||||||
|
lastActiveTab: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
105
e2e/specs/server_management/configure_server_modal.test.js
Normal file
105
e2e/specs/server_management/configure_server_modal.test.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('Configure Server Modal', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
await asyncSleep(1000);
|
||||||
|
|
||||||
|
this.app = await env.getApp();
|
||||||
|
|
||||||
|
configureServerModal = this.app.windows().find((window) => window.url().includes('welcomeScreen'));
|
||||||
|
await configureServerModal.click('#getStartedWelcomeScreen');
|
||||||
|
|
||||||
|
await asyncSleep(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
let configureServerModal;
|
||||||
|
|
||||||
|
it('MM-T5115 should not be valid if no display name has been set', async () => {
|
||||||
|
await configureServerModal.type('#input_name', '');
|
||||||
|
|
||||||
|
const connectButtonDisabled = await configureServerModal.getAttribute('#connectConfigureServer', 'disabled');
|
||||||
|
(connectButtonDisabled === '').should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T5116 should not be valid if no URL has been set', async () => {
|
||||||
|
await configureServerModal.type('#input_url', '');
|
||||||
|
|
||||||
|
const connectButtonDisabled = await configureServerModal.getAttribute('#connectConfigureServer', 'disabled');
|
||||||
|
(connectButtonDisabled === '').should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T5117 should be valid if display name and URL are set', async () => {
|
||||||
|
await configureServerModal.type('#input_name', 'TestServer');
|
||||||
|
await configureServerModal.type('#input_url', 'https://community.mattermost.com');
|
||||||
|
await configureServerModal.waitForSelector('#customMessage_url.Input___success');
|
||||||
|
|
||||||
|
const connectButtonDisabled = await configureServerModal.getAttribute('#connectConfigureServer', 'disabled');
|
||||||
|
(connectButtonDisabled === '').should.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T5118 should not be valid if an invalid URL has been set', async () => {
|
||||||
|
await configureServerModal.type('#input_name', 'TestServer');
|
||||||
|
await configureServerModal.type('#input_url', '!@#$%^&*()');
|
||||||
|
await configureServerModal.waitForSelector('#customMessage_url.Input___error');
|
||||||
|
|
||||||
|
const errorClass = await configureServerModal.getAttribute('#customMessage_url', 'class');
|
||||||
|
errorClass.should.contain('Input___error');
|
||||||
|
|
||||||
|
const connectButtonDisabled = await configureServerModal.getAttribute('#connectConfigureServer', 'disabled');
|
||||||
|
(connectButtonDisabled === '').should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T5119 should add the server to the config file', async () => {
|
||||||
|
await configureServerModal.type('#input_name', 'TestServer');
|
||||||
|
await configureServerModal.type('#input_url', 'http://example.org');
|
||||||
|
|
||||||
|
await configureServerModal.click('#connectConfigureServer');
|
||||||
|
|
||||||
|
await asyncSleep(1000);
|
||||||
|
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('welcomeScreen')));
|
||||||
|
existing.should.be.false;
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||||
|
savedConfig.teams.should.deep.contain({
|
||||||
|
url: 'http://example.org/',
|
||||||
|
name: 'TestServer',
|
||||||
|
order: 0,
|
||||||
|
lastActiveTab: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
172
e2e/specs/server_management/drag_and_drop.test.js
Normal file
172
e2e/specs/server_management/drag_and_drop.test.js
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('server_management/drag_and_drop', function desc() {
|
||||||
|
const config = {
|
||||||
|
...env.demoConfig,
|
||||||
|
teams: [
|
||||||
|
...env.demoConfig.teams,
|
||||||
|
{
|
||||||
|
name: 'google',
|
||||||
|
url: 'https://google.com/',
|
||||||
|
order: 2,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTeam: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeFunc = async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
const afterFunc = async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
describe('MM-T2634 should be able to drag and drop servers in the dropdown menu', async () => {
|
||||||
|
let mainWindow;
|
||||||
|
let dropdownView;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
await mainWindow.click('.ServerDropdownButton');
|
||||||
|
});
|
||||||
|
after(afterFunc);
|
||||||
|
|
||||||
|
it('MM-T2634_1 should appear the original order', async () => {
|
||||||
|
const firstMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(1) .ServerDropdown__draggable-handle');
|
||||||
|
const firstMenuItemText = await firstMenuItem.innerText();
|
||||||
|
firstMenuItemText.should.equal('example');
|
||||||
|
const secondMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(2) .ServerDropdown__draggable-handle');
|
||||||
|
const secondMenuItemText = await secondMenuItem.innerText();
|
||||||
|
secondMenuItemText.should.equal('github');
|
||||||
|
const thirdMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(3) .ServerDropdown__draggable-handle');
|
||||||
|
const thirdMenuItemText = await thirdMenuItem.innerText();
|
||||||
|
thirdMenuItemText.should.equal('google');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2634_2 after dragging the server down, should appear in the new order', async () => {
|
||||||
|
// Move the first server down, then re-open the dropdown
|
||||||
|
const initialMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(1) .ServerDropdown__draggable-handle');
|
||||||
|
await initialMenuItem.focus();
|
||||||
|
await dropdownView.keyboard.down(' ');
|
||||||
|
await dropdownView.keyboard.down('ArrowDown');
|
||||||
|
await dropdownView.keyboard.down(' ');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
await mainWindow.keyboard.press('Escape');
|
||||||
|
await mainWindow.click('.ServerDropdownButton');
|
||||||
|
|
||||||
|
// Verify that the new order persists
|
||||||
|
const firstMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(1) .ServerDropdown__draggable-handle');
|
||||||
|
const firstMenuItemText = await firstMenuItem.innerText();
|
||||||
|
firstMenuItemText.should.equal('github');
|
||||||
|
const secondMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(2) .ServerDropdown__draggable-handle');
|
||||||
|
const secondMenuItemText = await secondMenuItem.innerText();
|
||||||
|
secondMenuItemText.should.equal('example');
|
||||||
|
const thirdMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(3) .ServerDropdown__draggable-handle');
|
||||||
|
const thirdMenuItemText = await thirdMenuItem.innerText();
|
||||||
|
thirdMenuItemText.should.equal('google');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2634_3 should update the config file', () => {
|
||||||
|
// Verify config is updated
|
||||||
|
const newConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
const order0 = newConfig.teams.find((team) => team.name === 'github');
|
||||||
|
order0.order.should.equal(0);
|
||||||
|
const order1 = newConfig.teams.find((team) => team.name === 'example');
|
||||||
|
order1.order.should.equal(1);
|
||||||
|
const order2 = newConfig.teams.find((team) => team.name === 'google');
|
||||||
|
order2.order.should.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MM-T2635 should be able to drag and drop tabs', async () => {
|
||||||
|
let mainWindow;
|
||||||
|
before(async () => {
|
||||||
|
await beforeFunc();
|
||||||
|
mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
});
|
||||||
|
after(afterFunc);
|
||||||
|
|
||||||
|
it('MM-T2635_1 should be in the original order', async () => {
|
||||||
|
// Verify the original order
|
||||||
|
const firstTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(1)');
|
||||||
|
const firstTabText = await firstTab.innerText();
|
||||||
|
firstTabText.should.equal('Channels');
|
||||||
|
const secondTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(2)');
|
||||||
|
const secondTabText = await secondTab.innerText();
|
||||||
|
secondTabText.should.equal('Boards');
|
||||||
|
const thirdTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(3)');
|
||||||
|
const thirdTabText = await thirdTab.innerText();
|
||||||
|
thirdTabText.should.equal('Playbooks');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2635_2 after moving the tab to the right, the tab should be in the new order', async () => {
|
||||||
|
// Move the first tab to the right
|
||||||
|
let firstTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(1)');
|
||||||
|
await firstTab.focus();
|
||||||
|
await mainWindow.keyboard.down(' ');
|
||||||
|
await mainWindow.keyboard.down('ArrowRight');
|
||||||
|
await mainWindow.keyboard.down(' ');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
|
||||||
|
// Verify that the new order is visible
|
||||||
|
firstTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(1)');
|
||||||
|
const firstTabText = await firstTab.innerText();
|
||||||
|
firstTabText.should.equal('Boards');
|
||||||
|
const secondTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(2)');
|
||||||
|
const secondTabText = await secondTab.innerText();
|
||||||
|
secondTabText.should.equal('Channels');
|
||||||
|
const thirdTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(3)');
|
||||||
|
const thirdTabText = await thirdTab.innerText();
|
||||||
|
thirdTabText.should.equal('Playbooks');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2635_3 should update the config file', () => {
|
||||||
|
// Verify config is updated
|
||||||
|
const newConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
const firstTeam = newConfig.teams.find((team) => team.name === 'google');
|
||||||
|
const order0 = firstTeam.tabs.find((tab) => tab.name === 'TAB_FOCALBOARD');
|
||||||
|
order0.order.should.equal(0);
|
||||||
|
const order1 = firstTeam.tabs.find((tab) => tab.name === 'TAB_MESSAGING');
|
||||||
|
order1.order.should.equal(1);
|
||||||
|
const order2 = firstTeam.tabs.find((tab) => tab.name === 'TAB_PLAYBOOKS');
|
||||||
|
order2.order.should.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
265
e2e/specs/server_management/edit_server_modal.test.js
Normal file
265
e2e/specs/server_management/edit_server_modal.test.js
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('EditServerModal', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
|
||||||
|
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
await mainView.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.hover('.ServerDropdown .ServerDropdown__button:nth-child(1)');
|
||||||
|
await dropdownView.click('.ServerDropdown .ServerDropdown__button:nth-child(1) button.ServerDropdown__button-edit');
|
||||||
|
|
||||||
|
editServerView = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('editServer'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
let editServerView;
|
||||||
|
|
||||||
|
it('should not edit server when Cancel is pressed', async () => {
|
||||||
|
await editServerView.click('#cancelNewServerModal');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||||
|
savedConfig.teams.should.deep.contain({
|
||||||
|
name: 'example',
|
||||||
|
url: env.exampleURL,
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4391_1 should not edit server when Save is pressed but nothing edited', async () => {
|
||||||
|
await editServerView.click('#saveNewServerModal');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||||
|
savedConfig.teams.should.deep.contain({
|
||||||
|
name: 'example',
|
||||||
|
url: env.exampleURL,
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2826_3 should not edit server if an invalid server address has been set', async () => {
|
||||||
|
await editServerView.type('#serverUrlInput', 'superInvalid url');
|
||||||
|
await editServerView.waitForSelector('#urlValidation.error');
|
||||||
|
const existing = await editServerView.isVisible('#urlValidation.error');
|
||||||
|
existing.should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4391_2 should edit server when Save is pressed and name edited', async () => {
|
||||||
|
await editServerView.fill('#serverNameInput', 'NewTestServer');
|
||||||
|
await editServerView.click('#saveNewServerModal');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||||
|
savedConfig.teams.should.not.deep.contain({
|
||||||
|
name: 'example',
|
||||||
|
url: env.exampleURL,
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
});
|
||||||
|
savedConfig.teams.should.deep.contain({
|
||||||
|
name: 'NewTestServer',
|
||||||
|
url: env.exampleURL,
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4391_3 should edit server when Save is pressed and URL edited', async () => {
|
||||||
|
await editServerView.fill('#serverUrlInput', 'http://google.com');
|
||||||
|
await editServerView.click('#saveNewServerModal');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||||
|
savedConfig.teams.should.not.deep.contain({
|
||||||
|
name: 'example',
|
||||||
|
url: env.exampleURL,
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
});
|
||||||
|
savedConfig.teams.should.deep.contain({
|
||||||
|
name: 'example',
|
||||||
|
url: 'http://google.com/',
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4391_4 should edit server when Save is pressed and both edited', async () => {
|
||||||
|
await editServerView.fill('#serverNameInput', 'NewTestServer');
|
||||||
|
await editServerView.fill('#serverUrlInput', 'http://google.com');
|
||||||
|
await editServerView.click('#saveNewServerModal');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||||
|
savedConfig.teams.should.not.deep.contain({
|
||||||
|
name: 'example',
|
||||||
|
url: env.exampleURL,
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
});
|
||||||
|
savedConfig.teams.should.deep.contain({
|
||||||
|
name: 'NewTestServer',
|
||||||
|
url: 'http://google.com/',
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'TAB_MESSAGING',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_FOCALBOARD',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TAB_PLAYBOOKS',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lastActiveTab: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
57
e2e/specs/server_management/header.test.js
Normal file
57
e2e/specs/server_management/header.test.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('header', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
describe('MM-T2637 Double-Clicking on the header should minimize/maximize the app', async () => {
|
||||||
|
let header;
|
||||||
|
let browserWindow;
|
||||||
|
let initialBounds;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
initialBounds = {x: 0, y: 0, width: 800, height: 400, maximized: false};
|
||||||
|
fs.writeFileSync(env.boundsInfoPath, JSON.stringify(initialBounds));
|
||||||
|
this.app = await env.getApp();
|
||||||
|
const mainWindow = await this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
header = await mainWindow.locator('div.topBar');
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
try {
|
||||||
|
await this.app.close();
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2637_1 should maximize on double-clicking the header', async () => {
|
||||||
|
const headerBounds = await header.boundingBox();
|
||||||
|
await header.dblclick({position: {x: headerBounds.width / 2, y: headerBounds.y / 2}});
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const isMaximized = await browserWindow.evaluate((window) => window.isMaximized());
|
||||||
|
isMaximized.should.be.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T2637_2 should restore on double-clicking the header when maximized', async () => {
|
||||||
|
const maximizedHeaderBounds = await header.boundingBox();
|
||||||
|
await header.dblclick({position: {x: maximizedHeaderBounds.width / 2, y: maximizedHeaderBounds.y / 2}});
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const revertedBounds = await browserWindow.evaluate((window) => window.getContentBounds());
|
||||||
|
revertedBounds.height.should.be.equal(initialBounds.height);
|
||||||
|
revertedBounds.width.should.be.equal(initialBounds.width);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
76
e2e/specs/server_management/long_server_name.test.js
Normal file
76
e2e/specs/server_management/long_server_name.test.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('LongServerName', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
const config = env.demoConfig;
|
||||||
|
const longServerName = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus quis malesuada dolor, vel scelerisque sem';
|
||||||
|
const longServerUrl = 'https://example.org';
|
||||||
|
let newServerView;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
|
||||||
|
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
|
||||||
|
await mainView.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.click('.ServerDropdown .ServerDropdown__button.addServer');
|
||||||
|
newServerView = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('newServer'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for autofocus to finish
|
||||||
|
await asyncSleep(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4050 Long server name', async () => {
|
||||||
|
await newServerView.type('#serverNameInput', longServerName);
|
||||||
|
await newServerView.type('#serverUrlInput', longServerUrl);
|
||||||
|
await newServerView.click('#saveNewServerModal');
|
||||||
|
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(this.app.windows().find((window) => window.url().includes('newServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
|
||||||
|
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
|
||||||
|
const isServerTabExists = Boolean(mainView.locator(`text=${longServerName}`));
|
||||||
|
const isServerAddedDropdown = Boolean(dropdownView.locator(`text=${longServerName}`));
|
||||||
|
isServerTabExists.should.be.true;
|
||||||
|
isServerAddedDropdown.should.be.true;
|
||||||
|
|
||||||
|
const serverNameLocator = mainView.locator(`text=${longServerName}`);
|
||||||
|
|
||||||
|
const isTruncated = await serverNameLocator.evaluate((element) => {
|
||||||
|
return element.offsetWidth < element.scrollWidth;
|
||||||
|
});
|
||||||
|
isTruncated.should.be.true;
|
||||||
|
|
||||||
|
const isWithinMaxWidth = await serverNameLocator.evaluate((element) => {
|
||||||
|
const width = parseFloat(window.getComputedStyle(element).getPropertyValue('width'));
|
||||||
|
|
||||||
|
return width <= 400;
|
||||||
|
});
|
||||||
|
isWithinMaxWidth.should.be.true;
|
||||||
|
});
|
||||||
|
});
|
82
e2e/specs/server_management/remove_server_modal.test.js
Normal file
82
e2e/specs/server_management/remove_server_modal.test.js
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('RemoveServerModal', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
|
||||||
|
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
|
await mainView.click('.ServerDropdownButton');
|
||||||
|
await dropdownView.hover('.ServerDropdown .ServerDropdown__button:nth-child(1)');
|
||||||
|
await dropdownView.click('.ServerDropdown .ServerDropdown__button:nth-child(1) button.ServerDropdown__button-remove');
|
||||||
|
|
||||||
|
removeServerView = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('removeServer'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for autofocus to finish
|
||||||
|
await asyncSleep(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
let removeServerView;
|
||||||
|
|
||||||
|
it('MM-T4390_1 should remove existing server on click Remove', async () => {
|
||||||
|
await removeServerView.click('button:has-text("Remove")');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
|
||||||
|
const expectedConfig = JSON.parse(JSON.stringify(config.teams.slice(1)));
|
||||||
|
expectedConfig.forEach((value) => {
|
||||||
|
value.order--;
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||||
|
savedConfig.teams.should.deep.equal(expectedConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4390_2 should NOT remove existing server on click Cancel', async () => {
|
||||||
|
await removeServerView.click('button:has-text("Cancel")');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||||
|
savedConfig.teams.should.deep.equal(config.teams);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4390_3 should disappear on click Close', async () => {
|
||||||
|
await removeServerView.click('button.close');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('removeServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4390_4 should disappear on click background', async () => {
|
||||||
|
// ignore any target closed error
|
||||||
|
try {
|
||||||
|
await removeServerView.click('.modal', {position: {x: 20, y: 20}});
|
||||||
|
} catch {} // eslint-disable-line no-empty
|
||||||
|
await asyncSleep(1000);
|
||||||
|
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('removeServer')));
|
||||||
|
existing.should.be.false;
|
||||||
|
});
|
||||||
|
});
|
244
e2e/specs/settings.test.js
Normal file
244
e2e/specs/settings.test.js
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const {SHOW_SETTINGS_WINDOW} = require('src/common/communication');
|
||||||
|
|
||||||
|
const env = require('../modules/environment');
|
||||||
|
const {asyncSleep} = require('../modules/utils');
|
||||||
|
|
||||||
|
describe('Settings', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
fs.writeFileSync(env.appUpdatePath, '');
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Options', () => {
|
||||||
|
describe('Start app on login', () => {
|
||||||
|
it('MM-T4392 should appear on win32 or linux', async () => {
|
||||||
|
const expected = (process.platform === 'win32' || process.platform === 'linux');
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
await settingsWindow.waitForSelector('#inputAutoStart', {state: expected ? 'attached' : 'detached'});
|
||||||
|
const existing = await settingsWindow.isVisible('#inputAutoStart');
|
||||||
|
existing.should.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Show icon in menu bar / notification area', () => {
|
||||||
|
it('MM-T4393_1 should appear on darwin or linux', async () => {
|
||||||
|
const expected = (process.platform === 'darwin' || process.platform === 'linux');
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
await settingsWindow.waitForSelector('#inputShowTrayIcon', {state: expected ? 'attached' : 'detached'});
|
||||||
|
const existing = await settingsWindow.isVisible('#inputShowTrayIcon');
|
||||||
|
existing.should.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Save tray icon setting on mac', () => {
|
||||||
|
env.shouldTest(it, env.isOneOf(['darwin', 'linux']))('MM-T4393_2 should be saved when it\'s selected', async () => {
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
await settingsWindow.click('#inputShowTrayIcon');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
||||||
|
|
||||||
|
let config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
config0.showTrayIcon.should.true;
|
||||||
|
|
||||||
|
await settingsWindow.click('#inputShowTrayIcon');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
||||||
|
|
||||||
|
config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
config0.showTrayIcon.should.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Save tray icon theme on linux', () => {
|
||||||
|
env.shouldTest(it, process.platform === 'linux')('MM-T4393_3 should be saved when it\'s selected', async () => {
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
await settingsWindow.click('#inputShowTrayIcon');
|
||||||
|
await settingsWindow.click('input[value="dark"]');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
||||||
|
|
||||||
|
const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
config0.trayIconTheme.should.equal('dark');
|
||||||
|
|
||||||
|
await settingsWindow.click('input[value="light"]');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
||||||
|
|
||||||
|
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
config1.trayIconTheme.should.equal('light');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Leave app running in notification area when application window is closed', () => {
|
||||||
|
it('MM-T4394 should appear on linux and win32', async () => {
|
||||||
|
const expected = (process.platform === 'linux' || process.platform === 'win32');
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
const existing = await settingsWindow.isVisible('#inputMinimizeToTray');
|
||||||
|
existing.should.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Flash app window and taskbar icon when a new message is received', () => {
|
||||||
|
it('MM-T4395 should appear on win32 and linux', async () => {
|
||||||
|
const expected = (process.platform === 'win32' || process.platform === 'linux');
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
const existing = await settingsWindow.isVisible('#inputflashWindow');
|
||||||
|
existing.should.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Show red badge on taskbar icon to indicate unread messages', () => {
|
||||||
|
it('MM-T4396 should appear on darwin or win32', async () => {
|
||||||
|
const expected = (process.platform === 'darwin' || process.platform === 'win32');
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
const existing = await settingsWindow.isVisible('#inputShowUnreadBadge');
|
||||||
|
existing.should.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Check spelling', () => {
|
||||||
|
it('MM-T4397 should appear and be selectable', async () => {
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
const existing = await settingsWindow.isVisible('#inputSpellChecker');
|
||||||
|
existing.should.equal(true);
|
||||||
|
|
||||||
|
const selected = await settingsWindow.isChecked('#inputSpellChecker');
|
||||||
|
selected.should.equal(true);
|
||||||
|
|
||||||
|
await settingsWindow.click('#inputSpellChecker');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
||||||
|
|
||||||
|
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
config1.useSpellChecker.should.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Enable GPU hardware acceleration', () => {
|
||||||
|
it('MM-T4398 should save selected option', async () => {
|
||||||
|
const ID_INPUT_ENABLE_HARDWARE_ACCELERATION = '#inputEnableHardwareAcceleration';
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
const selected = await settingsWindow.isChecked(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
|
||||||
|
selected.should.equal(true); // default is true
|
||||||
|
|
||||||
|
await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
||||||
|
const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
config0.enableHardwareAcceleration.should.equal(false);
|
||||||
|
|
||||||
|
await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
||||||
|
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
config1.enableHardwareAcceleration.should.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
describe('Enable automatic check for updates', () => {
|
||||||
|
it('MM-T4549 should save selected option', async () => {
|
||||||
|
const ID_INPUT_ENABLE_AUTO_UPDATES = '#inputAutoCheckForUpdates';
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
const selected = await settingsWindow.isChecked(ID_INPUT_ENABLE_AUTO_UPDATES);
|
||||||
|
selected.should.equal(true); // default is true
|
||||||
|
|
||||||
|
await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES);
|
||||||
|
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saved")');
|
||||||
|
const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
config0.autoCheckForUpdates.should.equal(false);
|
||||||
|
|
||||||
|
await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES);
|
||||||
|
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saved")');
|
||||||
|
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
|
config1.autoCheckForUpdates.should.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
99
e2e/specs/settings/keyboard_shortcuts.test.js
Normal file
99
e2e/specs/settings/keyboard_shortcuts.test.js
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const robot = require('robotjs');
|
||||||
|
const {SHOW_SETTINGS_WINDOW} = require('src/common/communication');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('settings/keyboard_shortcuts', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
const config = env.demoConfig;
|
||||||
|
let settingsWindow;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
await asyncSleep(1000);
|
||||||
|
this.app = await env.getApp();
|
||||||
|
|
||||||
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
|
ipcMain.emit(showWindow);
|
||||||
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
|
settingsWindow = await this.app.waitForEvent('window', {
|
||||||
|
predicate: (window) => window.url().includes('settings'),
|
||||||
|
});
|
||||||
|
await settingsWindow.waitForSelector('.settingsPage.container');
|
||||||
|
|
||||||
|
const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown');
|
||||||
|
await textbox.scrollIntoViewIfNeeded();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MM-T1288 Manipulating Text', () => {
|
||||||
|
it('MM-T1288_1 should be able to select and deselect language in the settings window', async () => {
|
||||||
|
let textboxString;
|
||||||
|
await settingsWindow.click('#inputSpellCheckerLocalesDropdown');
|
||||||
|
await settingsWindow.type('#inputSpellCheckerLocalesDropdown', 'Afrikaans');
|
||||||
|
robot.keyTap('tab');
|
||||||
|
|
||||||
|
await settingsWindow.isVisible('#appOptionsSaveIndicator');
|
||||||
|
|
||||||
|
textboxString = await settingsWindow.innerText('div.SettingsPage__spellCheckerLocalesDropdown__multi-value__label');
|
||||||
|
textboxString.should.equal('Afrikaans');
|
||||||
|
|
||||||
|
await settingsWindow.isVisible('#appOptionsSaveIndicator');
|
||||||
|
|
||||||
|
await settingsWindow.click('[aria-label="Remove Afrikaans"]');
|
||||||
|
|
||||||
|
await settingsWindow.isVisible('#appOptionsSaveIndicator');
|
||||||
|
|
||||||
|
textboxString = await settingsWindow.inputValue('#inputSpellCheckerLocalesDropdown');
|
||||||
|
textboxString.should.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T1288_2 should be able to cut and paste in the settings window', async () => {
|
||||||
|
const textToCopy = 'Afrikaans';
|
||||||
|
env.clipboard(textToCopy);
|
||||||
|
|
||||||
|
const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown');
|
||||||
|
|
||||||
|
await textbox.selectText({force: true});
|
||||||
|
robot.keyTap('x', [env.cmdOrCtrl]);
|
||||||
|
let textValue = await textbox.getAttribute('value');
|
||||||
|
textValue.should.equal('');
|
||||||
|
|
||||||
|
await textbox.focus();
|
||||||
|
robot.keyTap('v', [env.cmdOrCtrl]);
|
||||||
|
textValue = await textbox.getAttribute('value');
|
||||||
|
textValue.trim().should.equal('Afrikaans');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T1288_3 should be able to copy and paste in the settings window', async () => {
|
||||||
|
const textToCopy = 'Afrikaans';
|
||||||
|
env.clipboard(textToCopy);
|
||||||
|
|
||||||
|
const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown');
|
||||||
|
|
||||||
|
await textbox.selectText({force: true});
|
||||||
|
robot.keyTap('c', [env.cmdOrCtrl]);
|
||||||
|
await textbox.focus();
|
||||||
|
await textbox.type('other-text');
|
||||||
|
robot.keyTap('v', [env.cmdOrCtrl]);
|
||||||
|
const textValue = await textbox.getAttribute('value');
|
||||||
|
textValue.trim().should.equal('other-textAfrikaans');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
65
e2e/specs/startup/app.test.js
Normal file
65
e2e/specs/startup/app.test.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('startup/app', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
this.app = await env.getApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4400 should be stopped when the app instance already exists', (done) => {
|
||||||
|
const secondApp = env.getApp();
|
||||||
|
|
||||||
|
// In the correct case, 'start().then' is not called.
|
||||||
|
// So need to use setTimeout in order to finish this test.
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
done();
|
||||||
|
}, 3000);
|
||||||
|
secondApp.then(() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
return secondApp.close();
|
||||||
|
}).then(() => {
|
||||||
|
done(new Error('Second app instance exists'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4975 should show the welcome screen modal when no servers exist', async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
this.app = await env.getApp();
|
||||||
|
|
||||||
|
await asyncSleep(500);
|
||||||
|
|
||||||
|
const welcomeScreenModal = this.app.windows().find((window) => window.url().includes('welcomeScreen'));
|
||||||
|
const modalButton = await welcomeScreenModal.innerText('.WelcomeScreen .WelcomeScreen__button');
|
||||||
|
modalButton.should.equal('Get Started');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.platform !== 'linux') {
|
||||||
|
it('MM-T4985 should show app name in title bar when no servers exist', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const titleBarText = await mainWindow.innerText('.app-title');
|
||||||
|
titleBarText.should.equal('Electron');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
77
e2e/specs/startup/config.test.js
Normal file
77
e2e/specs/startup/config.test.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
|
||||||
|
describe('config', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
try {
|
||||||
|
await this.app.close();
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MM-T4401 should show servers in dropdown when there is config file', async () => {
|
||||||
|
const config = env.demoConfig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
|
this.app = await env.getApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
try {
|
||||||
|
await this.app.close();
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4401_1 should show correct server in the dropdown button', async () => {
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton');
|
||||||
|
dropdownButtonText.should.equal('example');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4401_2 should set src of browser view from config file', async () => {
|
||||||
|
const firstServer = this.app.windows().find((window) => window.url() === config.teams[0].url);
|
||||||
|
const secondServer = this.app.windows().find((window) => window.url() === config.teams[1].url);
|
||||||
|
|
||||||
|
firstServer.should.not.be.null;
|
||||||
|
secondServer.should.not.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4402 should upgrade v0 config file', async () => {
|
||||||
|
const Config = require('src/common/config').Config;
|
||||||
|
const newConfig = new Config(env.configFilePath);
|
||||||
|
const oldConfig = {
|
||||||
|
url: env.exampleURL,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(oldConfig));
|
||||||
|
this.app = await env.getApp();
|
||||||
|
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton:has-text("Primary server")');
|
||||||
|
dropdownButtonText.should.equal('Primary server');
|
||||||
|
|
||||||
|
const str = fs.readFileSync(env.configFilePath, 'utf8');
|
||||||
|
const upgradedConfig = JSON.parse(str);
|
||||||
|
upgradedConfig.version.should.equal(newConfig.defaultData.version);
|
||||||
|
await this.app.close();
|
||||||
|
});
|
||||||
|
});
|
154
e2e/specs/startup/welcome_screen_modal.test.js
Normal file
154
e2e/specs/startup/welcome_screen_modal.test.js
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
const {asyncSleep} = require('../../modules/utils');
|
||||||
|
|
||||||
|
describe('Welcome Screen Modal', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
await asyncSleep(1000);
|
||||||
|
|
||||||
|
this.app = await env.getApp();
|
||||||
|
|
||||||
|
welcomeScreenModal = this.app.windows().find((window) => window.url().includes('welcomeScreen'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
await this.app.close();
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
let welcomeScreenModal;
|
||||||
|
|
||||||
|
it('MM-T4976 should show the slides in the expected order', async () => {
|
||||||
|
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
|
||||||
|
welcomeSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
const welcomeSlideTitle = await welcomeScreenModal.innerText('#welcome .WelcomeScreenSlide__title');
|
||||||
|
welcomeSlideTitle.should.equal('Welcome');
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
|
||||||
|
const channelSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
channelSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
const channelSlideTitle = await welcomeScreenModal.innerText('div.Carousel__slide.inFromRight .WelcomeScreenSlide__title');
|
||||||
|
channelSlideTitle.should.equal('Collaborate in real time');
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
|
||||||
|
const callsSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
callsSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
const callsSlideTitle = await welcomeScreenModal.innerText('div.Carousel__slide.inFromRight .WelcomeScreenSlide__title');
|
||||||
|
callsSlideTitle.should.equal('Start secure calls instantly');
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
|
||||||
|
const integrationSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
integrationSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
const integrationSlideTitle = await welcomeScreenModal.innerText('div.Carousel__slide.inFromRight .WelcomeScreenSlide__title');
|
||||||
|
integrationSlideTitle.should.equal('Integrate with tools you love');
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4977 should be able to move through slides clicking the navigation buttons', async () => {
|
||||||
|
let welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
|
||||||
|
welcomeSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
|
||||||
|
const channelSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
channelSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
|
||||||
|
await welcomeScreenModal.click('#prevCarouselButton');
|
||||||
|
|
||||||
|
welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
|
||||||
|
welcomeSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4978 should be able to move through slides clicking the pagination indicator', async () => {
|
||||||
|
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
|
||||||
|
welcomeSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
|
||||||
|
await welcomeScreenModal.click('#PaginationIndicator3');
|
||||||
|
|
||||||
|
const integrationSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
integrationSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
|
||||||
|
await welcomeScreenModal.click('#PaginationIndicator2');
|
||||||
|
|
||||||
|
const callsSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromLeft', 'class');
|
||||||
|
callsSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4979 should be able to move forward through slides automatically every 5 seconds', async () => {
|
||||||
|
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
|
||||||
|
welcomeSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
|
||||||
|
await asyncSleep(5500);
|
||||||
|
|
||||||
|
const channelSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
channelSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
const channelSlideTitle = await welcomeScreenModal.innerText('div.Carousel__slide.inFromRight .WelcomeScreenSlide__title');
|
||||||
|
channelSlideTitle.should.equal('Collaborate in real time');
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4980 should show the slides in the expected order', async () => {
|
||||||
|
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
|
||||||
|
welcomeSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
|
||||||
|
const channelSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
channelSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
|
||||||
|
const callsSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
callsSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
|
||||||
|
const integrationSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
integrationSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4981 should be able to move from last to first slide', async () => {
|
||||||
|
await welcomeScreenModal.click('#PaginationIndicator3');
|
||||||
|
|
||||||
|
const integrationSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromRight', 'class');
|
||||||
|
integrationSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
const integrationSlideTitle = await welcomeScreenModal.innerText('div.Carousel__slide.inFromRight .WelcomeScreenSlide__title');
|
||||||
|
integrationSlideTitle.should.equal('Integrate with tools you love');
|
||||||
|
|
||||||
|
await welcomeScreenModal.click('#nextCarouselButton');
|
||||||
|
|
||||||
|
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
|
||||||
|
welcomeSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4982 should be able to move from first to last slide', async () => {
|
||||||
|
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
|
||||||
|
welcomeSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
|
||||||
|
await welcomeScreenModal.click('#prevCarouselButton');
|
||||||
|
|
||||||
|
const integrationSlideClass = await welcomeScreenModal.getAttribute('div.Carousel__slide.inFromLeft', 'class');
|
||||||
|
integrationSlideClass.should.contain('Carousel__slide-current');
|
||||||
|
const integrationSlideTitle = await welcomeScreenModal.innerText('div.Carousel__slide.inFromLeft .WelcomeScreenSlide__title');
|
||||||
|
integrationSlideTitle.should.equal('Integrate with tools you love');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4983 should be able to click the get started button and be redirected to new server modal', async () => {
|
||||||
|
await welcomeScreenModal.click('#getStartedWelcomeScreen');
|
||||||
|
|
||||||
|
await asyncSleep(1000);
|
||||||
|
|
||||||
|
const modalCardTitle = await welcomeScreenModal.innerText('.ConfigureServer .ConfigureServer__card-title');
|
||||||
|
modalCardTitle.should.equal('Enter your server details');
|
||||||
|
});
|
||||||
|
});
|
61
e2e/specs/startup/window.test.js
Normal file
61
e2e/specs/startup/window.test.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const env = require('../../modules/environment');
|
||||||
|
|
||||||
|
describe('window', function desc() {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env.createTestUserDataDir();
|
||||||
|
env.cleanTestConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (this.app) {
|
||||||
|
try {
|
||||||
|
await this.app.close();
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
await env.clearElectronInstances();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: this fails on Linux right now due to the window frame for some reason
|
||||||
|
if (process.platform !== 'linux') {
|
||||||
|
it('MM-T4403_1 should restore window bounds', async () => {
|
||||||
|
const expectedBounds = {x: 100, y: 200, width: 800, height: 400};
|
||||||
|
fs.writeFileSync(env.boundsInfoPath, JSON.stringify(expectedBounds));
|
||||||
|
this.app = await env.getApp();
|
||||||
|
const mainWindow = await this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const bounds = await browserWindow.evaluate((window) => window.getContentBounds());
|
||||||
|
bounds.should.deep.equal(expectedBounds);
|
||||||
|
await this.app.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('MM-T4403_2 should NOT restore window bounds if x is located on outside of viewarea', async () => {
|
||||||
|
fs.writeFileSync(env.boundsInfoPath, JSON.stringify({x: -100000, y: 200, width: 800, height: 400}));
|
||||||
|
this.app = await env.getApp();
|
||||||
|
const mainWindow = await this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const bounds = await browserWindow.evaluate((window) => window.getContentBounds());
|
||||||
|
bounds.x.should.satisfy((x) => (x > -100000));
|
||||||
|
await this.app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MM-T4403_3 should NOT restore window bounds if y is located on outside of viewarea', async () => {
|
||||||
|
fs.writeFileSync(env.boundsInfoPath, JSON.stringify({x: 100, y: 200000, width: 800, height: 400}));
|
||||||
|
this.app = await env.getApp();
|
||||||
|
const mainWindow = await this.app.windows().find((window) => window.url().includes('index'));
|
||||||
|
const browserWindow = await this.app.browserWindow(mainWindow);
|
||||||
|
const bounds = await browserWindow.evaluate((window) => window.getContentBounds());
|
||||||
|
bounds.y.should.satisfy((y) => (y < 200000));
|
||||||
|
await this.app.close();
|
||||||
|
});
|
||||||
|
});
|
76
e2e/utils/analyze-flaky-test.js
Normal file
76
e2e/utils/analyze-flaky-test.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const {MOCHAWESOME_REPORT_DIR} = require('./constants');
|
||||||
|
const knownFlakyTests = require('./known_flaky_tests.json');
|
||||||
|
const {
|
||||||
|
generateShortSummary,
|
||||||
|
readJsonFromFile,
|
||||||
|
} = require('./report');
|
||||||
|
|
||||||
|
function analyzeFlakyTests() {
|
||||||
|
const os = process.platform;
|
||||||
|
try {
|
||||||
|
// Import
|
||||||
|
const jsonReport = readJsonFromFile(path.join(MOCHAWESOME_REPORT_DIR, 'mochawesome.json'));
|
||||||
|
|
||||||
|
const {failedFullTitles} = generateShortSummary(jsonReport);
|
||||||
|
|
||||||
|
// Get the list of known flaky tests for the provided operating system
|
||||||
|
const knownFlakyTestsForOS = new Set(knownFlakyTests[os] || []);
|
||||||
|
|
||||||
|
// Filter out the known flaky tests from the failed test titles
|
||||||
|
const newFailedTests = failedFullTitles.filter((test) => !knownFlakyTestsForOS.has(test));
|
||||||
|
|
||||||
|
// Check if any known failed tests are fixed
|
||||||
|
const fixedTests = [...knownFlakyTestsForOS].filter((test) => !failedFullTitles.includes(test));
|
||||||
|
|
||||||
|
const commentBody = generateCommentBodyFunctionalTest(newFailedTests, fixedTests);
|
||||||
|
|
||||||
|
// Print on CI
|
||||||
|
console.log(commentBody);
|
||||||
|
|
||||||
|
return {commentBody, newFailedTests};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error analyzing failures:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCommentBodyFunctionalTest(newFailedTests, fixedTests) {
|
||||||
|
const osName = process.env.RUNNER_OS;
|
||||||
|
const build = process.env.BUILD_TAG;
|
||||||
|
|
||||||
|
let commentBody = `
|
||||||
|
## Test Summary for ${osName} on commit ${build}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (newFailedTests.length === 0 && fixedTests.length === 0) {
|
||||||
|
commentBody += `
|
||||||
|
All stable tests passed on ${osName}.
|
||||||
|
`;
|
||||||
|
return commentBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newFailedTests.length > 0) {
|
||||||
|
const newTestFailure = `New failed tests found on ${osName}:\n${newFailedTests.map((test) => `- ${test}`).join('\n')}`;
|
||||||
|
commentBody += `
|
||||||
|
${newTestFailure}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fixedTests.length > 0) {
|
||||||
|
const fixedTestMessage = `The following known failed tests have been fixed on ${osName}:\n\t${fixedTests.map((test) => `- ${test}`).join('\n\t')}`;
|
||||||
|
commentBody += `
|
||||||
|
${fixedTestMessage}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commentBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
analyzeFlakyTests,
|
||||||
|
};
|
88
e2e/utils/artifacts.js
Normal file
88
e2e/utils/artifacts.js
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/* eslint-disable no-console,consistent-return */
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const {S3} = require('@aws-sdk/client-s3');
|
||||||
|
const {Upload} = require('@aws-sdk/lib-storage');
|
||||||
|
const async = require('async');
|
||||||
|
const mime = require('mime-types');
|
||||||
|
const readdir = require('recursive-readdir');
|
||||||
|
|
||||||
|
const {MOCHAWESOME_REPORT_DIR} = require('./constants');
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const {
|
||||||
|
AWS_S3_BUCKET,
|
||||||
|
AWS_ACCESS_KEY_ID,
|
||||||
|
AWS_SECRET_ACCESS_KEY,
|
||||||
|
BUILD_ID,
|
||||||
|
BRANCH,
|
||||||
|
BUILD_TAG,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
const s3 = new S3({
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: AWS_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFiles(dirPath) {
|
||||||
|
return fs.existsSync(dirPath) ? readdir(dirPath) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveArtifacts() {
|
||||||
|
if (!AWS_S3_BUCKET || !AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) {
|
||||||
|
console.log('No AWS credentials found. Test artifacts not uploaded to S3.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const s3Folder = `${BUILD_ID}-${BRANCH}-${BUILD_TAG}`.replace(/\./g, '-');
|
||||||
|
const uploadPath = path.resolve(__dirname, `../../${MOCHAWESOME_REPORT_DIR}`);
|
||||||
|
const filesToUpload = await getFiles(uploadPath);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
async.eachOfLimit(
|
||||||
|
filesToUpload,
|
||||||
|
10,
|
||||||
|
async.asyncify(async (file) => {
|
||||||
|
const Key = file.replace(uploadPath, s3Folder).replaceAll('\\', '/');
|
||||||
|
const contentType = mime.lookup(file);
|
||||||
|
const charset = mime.charset(contentType);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Upload({
|
||||||
|
client: s3,
|
||||||
|
params: {
|
||||||
|
Key,
|
||||||
|
Bucket: AWS_S3_BUCKET,
|
||||||
|
Body: fs.readFileSync(file),
|
||||||
|
ContentType: `${contentType}${charset ? '; charset=' + charset : ''}`,
|
||||||
|
},
|
||||||
|
}).done();
|
||||||
|
return {success: true};
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Failed to upload artifact:', file);
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log('Failed to upload artifacts');
|
||||||
|
return reject(new Error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportLink = `https://${AWS_S3_BUCKET}.s3.amazonaws.com/${s3Folder}/mochawesome.html`;
|
||||||
|
resolve({success: true, reportLink});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {saveArtifacts};
|
10
e2e/utils/constants.js
Normal file
10
e2e/utils/constants.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
const MOCHAWESOME_REPORT_DIR = './mochawesome-report';
|
||||||
|
const PERFORMANCE_REPORT_DIR = './performance';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
MOCHAWESOME_REPORT_DIR,
|
||||||
|
PERFORMANCE_REPORT_DIR,
|
||||||
|
};
|
14
e2e/utils/known_flaky_tests.json
Normal file
14
e2e/utils/known_flaky_tests.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"darwin": [
|
||||||
|
"popup MM-T2827_1 should be able to select all in popup windows"
|
||||||
|
],
|
||||||
|
"linux": [
|
||||||
|
"menu/view MM-T820 should open Developer Tools For Application Wrapper for main window",
|
||||||
|
"Menu/window_menu MM-T824 should be minimized when keyboard shortcuts are pressed",
|
||||||
|
"Menu/window_menu MM-T825 should be hidden when keyboard shortcuts are pressed",
|
||||||
|
"header MM-T2637 Double-Clicking on the header should minimize/maximize the app MM-T2637_1 should maximize on double-clicking the header"
|
||||||
|
],
|
||||||
|
"win32": [
|
||||||
|
"application MM-T1304/MM-T1306 should open the app on the requested deep link"
|
||||||
|
]
|
||||||
|
}
|
36
e2e/utils/pr-e2e-durations-report.js
Normal file
36
e2e/utils/pr-e2e-durations-report.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
function generateCommentBodyPerformanceTest(fileContents) {
|
||||||
|
const data = JSON.parse(fileContents);
|
||||||
|
|
||||||
|
return `
|
||||||
|
E2E Performance Test results:
|
||||||
|
|
||||||
|
| Test | Duration |
|
||||||
|
| --- | --- |
|
||||||
|
${data?.passes?.reduce((acc, pass) => {
|
||||||
|
return `${acc}| ${pass.fullTitle || 'title'} | ${pass.duration}ms |\n`;
|
||||||
|
}, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
${data?.failures?.length > 0 ? `
|
||||||
|
Some tests failed:
|
||||||
|
| Test | Duration |
|
||||||
|
| --- | --- |
|
||||||
|
${data.failures.forEach((failure) => `| ${failure.fullTitle} | ${failure.duration}`)}
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Raw results</summary>
|
||||||
|
|
||||||
|
\`\`\`js
|
||||||
|
${fileContents}
|
||||||
|
\`\`\`
|
||||||
|
</details>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateCommentBodyPerformanceTest,
|
||||||
|
};
|
298
e2e/utils/report.js
Normal file
298
e2e/utils/report.js
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/* eslint-disable no-console, camelcase */
|
||||||
|
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
const fse = require('fs-extra');
|
||||||
|
|
||||||
|
const {MOCHAWESOME_REPORT_DIR} = require('./constants');
|
||||||
|
|
||||||
|
const package = require('../../package.json');
|
||||||
|
const e2ePackage = require('../package.json');
|
||||||
|
|
||||||
|
const MAX_FAILED_TITLES = 5;
|
||||||
|
|
||||||
|
let incrementalDuration = 0;
|
||||||
|
|
||||||
|
function getAllTests(results) {
|
||||||
|
const tests = [];
|
||||||
|
results.forEach((result) => {
|
||||||
|
result.tests.forEach((test) => {
|
||||||
|
incrementalDuration += test.duration;
|
||||||
|
tests.push({...test, incrementalDuration});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.suites.length > 0) {
|
||||||
|
getAllTests(result.suites).forEach((test) => tests.push(test));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateStatsFieldValue(stats, failedFullTitles) {
|
||||||
|
let statsFieldValue = `
|
||||||
|
| Key | Value |
|
||||||
|
|:---|:---|
|
||||||
|
| Passing Rate | ${stats.passPercent.toFixed(2)}% |
|
||||||
|
| Duration | ${(stats.duration / (60 * 1000)).toFixed(2)} mins |
|
||||||
|
| Suites | ${stats.suites} |
|
||||||
|
| Tests | ${stats.tests} |
|
||||||
|
| :white_check_mark: Passed | ${stats.passes} |
|
||||||
|
| :x: Failed | ${stats.failures} |
|
||||||
|
| :fast_forward: Skipped | ${stats.skipped} |
|
||||||
|
`;
|
||||||
|
|
||||||
|
// If present, add full title of failing tests.
|
||||||
|
// Only show per maximum number of failed titles with the last item as "more..." if failing tests are more than that.
|
||||||
|
let failedTests;
|
||||||
|
if (failedFullTitles && failedFullTitles.length > 0) {
|
||||||
|
const re = /[:'"\\]/gi;
|
||||||
|
const failed = failedFullTitles;
|
||||||
|
if (failed.length > MAX_FAILED_TITLES) {
|
||||||
|
failedTests = failed.slice(0, MAX_FAILED_TITLES - 1).map((f) => `- ${f.replace(re, '')}`).join('\n');
|
||||||
|
failedTests += '\n- more...';
|
||||||
|
} else {
|
||||||
|
failedTests = failed.map((f) => `- ${f.replace(re, '')}`).join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedTests) {
|
||||||
|
statsFieldValue += '###### Failed Tests:\n' + failedTests;
|
||||||
|
}
|
||||||
|
|
||||||
|
return statsFieldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateShortSummary(report) {
|
||||||
|
const {results, stats} = report;
|
||||||
|
const tests = getAllTests(results);
|
||||||
|
|
||||||
|
const failedFullTitles = tests.filter((t) => t.fail).map((t) => t.fullTitle);
|
||||||
|
const statsFieldValue = generateStatsFieldValue(stats, failedFullTitles);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats,
|
||||||
|
statsFieldValue,
|
||||||
|
failedFullTitles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeOldGeneratedReports() {
|
||||||
|
[
|
||||||
|
'all.json',
|
||||||
|
'summary.json',
|
||||||
|
'mochawesome.html',
|
||||||
|
].forEach((file) => fse.removeSync(`${MOCHAWESOME_REPORT_DIR}/${file}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJsonToFile(jsonObject, filename, dir) {
|
||||||
|
fse.writeJson(`${dir}/${filename}`, jsonObject).
|
||||||
|
then(() => console.log('Successfully written:', filename)).
|
||||||
|
catch((err) => console.error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonFromFile(file) {
|
||||||
|
try {
|
||||||
|
return fse.readJsonSync(file);
|
||||||
|
} catch (err) {
|
||||||
|
return {err};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOS() {
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
return 'macOS';
|
||||||
|
case 'win32':
|
||||||
|
return 'Windows';
|
||||||
|
case 'linux':
|
||||||
|
return 'Linux';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnvironmentValues() {
|
||||||
|
return {
|
||||||
|
playwrightVersion: e2ePackage.dependencies.playwright,
|
||||||
|
electronVersion: package.devDependencies.electron,
|
||||||
|
osName: getOS(),
|
||||||
|
osVersion: os.release(),
|
||||||
|
nodeVersion: process.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [
|
||||||
|
{status: 'Passed', priority: 'none', cutOff: 100, color: '#43A047'},
|
||||||
|
{status: 'Failed', priority: 'low', cutOff: 98, color: '#FFEB3B'},
|
||||||
|
{status: 'Failed', priority: 'medium', cutOff: 95, color: '#FF9800'},
|
||||||
|
{status: 'Failed', priority: 'high', cutOff: 0, color: '#F44336'},
|
||||||
|
];
|
||||||
|
|
||||||
|
function generateTestReport(summary, isUploadedToS3, reportLink, testCycleKey) {
|
||||||
|
const {
|
||||||
|
FULL_REPORT,
|
||||||
|
TEST_CYCLE_LINK_PREFIX,
|
||||||
|
} = process.env;
|
||||||
|
const {statsFieldValue, stats} = summary;
|
||||||
|
const {
|
||||||
|
playwrightVersion,
|
||||||
|
electronVersion,
|
||||||
|
osName,
|
||||||
|
osVersion,
|
||||||
|
nodeVersion,
|
||||||
|
} = getEnvironmentValues();
|
||||||
|
|
||||||
|
let testResult;
|
||||||
|
for (let i = 0; i < result.length; i++) {
|
||||||
|
if (stats.passPercent >= result[i].cutOff) {
|
||||||
|
testResult = result[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = generateTitle();
|
||||||
|
const envValue = `playwright@${playwrightVersion} | node@${nodeVersion} | electron@${electronVersion} | ${osName}@${osVersion}`;
|
||||||
|
|
||||||
|
if (FULL_REPORT === 'true') {
|
||||||
|
let reportField;
|
||||||
|
if (isUploadedToS3) {
|
||||||
|
reportField = {
|
||||||
|
short: false,
|
||||||
|
title: 'Test Report',
|
||||||
|
value: `[Link to the report](${reportLink})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let testCycleField;
|
||||||
|
if (testCycleKey) {
|
||||||
|
testCycleField = {
|
||||||
|
short: false,
|
||||||
|
title: 'Test Execution',
|
||||||
|
value: `[Recorded test executions](${TEST_CYCLE_LINK_PREFIX}${testCycleKey})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: 'Playwright UI Test',
|
||||||
|
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||||
|
attachments: [{
|
||||||
|
color: testResult.color,
|
||||||
|
author_name: 'Desktop End-to-end Testing',
|
||||||
|
author_icon: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||||
|
author_link: 'https://www.mattermost.com',
|
||||||
|
title,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
short: false,
|
||||||
|
title: 'Environment',
|
||||||
|
value: envValue,
|
||||||
|
},
|
||||||
|
reportField,
|
||||||
|
testCycleField,
|
||||||
|
{
|
||||||
|
short: false,
|
||||||
|
title: `Key metrics (required support: ${testResult.priority})`,
|
||||||
|
value: statsFieldValue,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let quickSummary = `${stats.passPercent.toFixed(2)}% (${stats.passes}/${stats.tests}) in ${stats.suites} suites`;
|
||||||
|
if (isUploadedToS3) {
|
||||||
|
quickSummary = `[${quickSummary}](${reportLink})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let testCycleLink = '';
|
||||||
|
if (testCycleKey) {
|
||||||
|
testCycleLink = testCycleKey ? `| [Recorded test executions](${TEST_CYCLE_LINK_PREFIX}${testCycleKey})` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: 'Playwright UI Test',
|
||||||
|
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||||
|
attachments: [{
|
||||||
|
color: testResult.color,
|
||||||
|
author_name: 'Desktop End-to-end Testing',
|
||||||
|
author_icon: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||||
|
author_link: 'https://www.mattermost.com/',
|
||||||
|
title,
|
||||||
|
text: `${quickSummary} | ${(stats.duration / (60 * 1000)).toFixed(2)} mins ${testCycleLink}\n${envValue}`,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTitle() {
|
||||||
|
const {
|
||||||
|
BRANCH,
|
||||||
|
DESKTOP_VERSION,
|
||||||
|
PULL_REQUEST,
|
||||||
|
RELEASE_VERSION,
|
||||||
|
SERVER_VERSION,
|
||||||
|
TYPE,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
let releaseVersion = '';
|
||||||
|
if (RELEASE_VERSION) {
|
||||||
|
releaseVersion = ` for ${RELEASE_VERSION}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title;
|
||||||
|
|
||||||
|
switch (TYPE) {
|
||||||
|
case 'PR':
|
||||||
|
title = `E2E for Pull Request Build: [${BRANCH}](${PULL_REQUEST})`;
|
||||||
|
break;
|
||||||
|
case 'RELEASE':
|
||||||
|
title = `E2E for Release Build${releaseVersion}`;
|
||||||
|
break;
|
||||||
|
case 'NIGHTLY':
|
||||||
|
title = 'E2E for Master Nightly Build';
|
||||||
|
break;
|
||||||
|
case 'MASTER':
|
||||||
|
title = 'E2E for Post Merge to Master';
|
||||||
|
break;
|
||||||
|
case 'MANUAL':
|
||||||
|
title = `E2E for Manually triggered for ${BRANCH}`;
|
||||||
|
break;
|
||||||
|
case 'CMT':
|
||||||
|
title = `Compatibility Matrix Testing Report for Server v${SERVER_VERSION} and Desktop version v${DESKTOP_VERSION}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = 'E2E for Build$';
|
||||||
|
}
|
||||||
|
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendReport(name, url, data) {
|
||||||
|
const requestOptions = {method: 'POST', url, data};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios(requestOptions);
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
console.log(`Successfully sent ${name}.`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (er) {
|
||||||
|
console.log(`Something went wrong while sending ${name}.`, er);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateShortSummary,
|
||||||
|
generateTestReport,
|
||||||
|
getAllTests,
|
||||||
|
removeOldGeneratedReports,
|
||||||
|
sendReport,
|
||||||
|
readJsonFromFile,
|
||||||
|
writeJsonToFile,
|
||||||
|
};
|
240
e2e/utils/test_cases.js
Normal file
240
e2e/utils/test_cases.js
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
// See reference: https://support.smartbear.com/tm4j-cloud/api-docs/
|
||||||
|
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
const chalk = require('chalk');
|
||||||
|
|
||||||
|
const {getAllTests} = require('./report');
|
||||||
|
|
||||||
|
const status = {
|
||||||
|
passed: 'Pass',
|
||||||
|
failed: 'Fail',
|
||||||
|
pending: 'Pending',
|
||||||
|
skipped: 'Skip',
|
||||||
|
};
|
||||||
|
|
||||||
|
const environment = {
|
||||||
|
chrome: 'Chrome',
|
||||||
|
firefox: 'Firefox',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStepStateResult(steps = []) {
|
||||||
|
return steps.reduce((acc, item) => {
|
||||||
|
if (acc[item.state]) {
|
||||||
|
acc[item.state] += 1;
|
||||||
|
} else {
|
||||||
|
acc[item.state] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepStateSummary(steps = []) {
|
||||||
|
const result = getStepStateResult(steps);
|
||||||
|
|
||||||
|
return Object.entries(result).map(([key, value]) => `${value} ${key}`).join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getZEPHYRTestCases(report) {
|
||||||
|
return getAllTests(report.results).
|
||||||
|
filter((item) => /^(MM-T)\w+/g.test(item.title)). // eslint-disable-line wrap-regex
|
||||||
|
map((item) => {
|
||||||
|
return {
|
||||||
|
title: item.title,
|
||||||
|
duration: item.duration,
|
||||||
|
incrementalDuration: item.incrementalDuration,
|
||||||
|
state: item.state,
|
||||||
|
pass: item.pass,
|
||||||
|
fail: item.fail,
|
||||||
|
pending: item.pending,
|
||||||
|
};
|
||||||
|
}).
|
||||||
|
reduce((acc, item) => {
|
||||||
|
// Extract the key to exactly match with "MM-T[0-9]+"
|
||||||
|
const key = item.title.match(/(MM-T\d+)/)[0];
|
||||||
|
|
||||||
|
if (acc[key]) {
|
||||||
|
acc[key].push(item);
|
||||||
|
} else {
|
||||||
|
acc[key] = [item];
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveToEndpoint(url, data) {
|
||||||
|
return axios({
|
||||||
|
method: 'POST',
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
Authorization: process.env.ZEPHYR_API_KEY,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log('Something went wrong:', error.response.data);
|
||||||
|
return error.response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getZEPHYRFolderID() {
|
||||||
|
const {
|
||||||
|
TYPE,
|
||||||
|
ZEPHYR_FOLDER_ID,
|
||||||
|
ZEPHYR_FOLDER_LINUX_REPORT,
|
||||||
|
ZEPHYR_FOLDER_MACOS_REPORT,
|
||||||
|
ZEPHYR_FOLDER_WIN_REPORT,
|
||||||
|
} = process.env;
|
||||||
|
if (TYPE === 'MASTER') {
|
||||||
|
return ZEPHYR_FOLDER_ID;
|
||||||
|
}
|
||||||
|
const platform = os.platform();
|
||||||
|
|
||||||
|
// Define Zephyr folder IDs for different run types and platforms.
|
||||||
|
// For PR we dont generate reports.
|
||||||
|
// Post Merge to master branch, default folderID will be used.
|
||||||
|
const folderIDs = {
|
||||||
|
RELEASE: {
|
||||||
|
darwin: ZEPHYR_FOLDER_MACOS_REPORT,
|
||||||
|
win32: ZEPHYR_FOLDER_WIN_REPORT,
|
||||||
|
linux: ZEPHYR_FOLDER_LINUX_REPORT,
|
||||||
|
default: ZEPHYR_FOLDER_ID,
|
||||||
|
},
|
||||||
|
NIGHTLY: {
|
||||||
|
darwin: ZEPHYR_FOLDER_MACOS_REPORT,
|
||||||
|
win32: ZEPHYR_FOLDER_WIN_REPORT,
|
||||||
|
linux: ZEPHYR_FOLDER_LINUX_REPORT,
|
||||||
|
default: ZEPHYR_FOLDER_ID,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the folder ID based on the type and platform
|
||||||
|
const typeFolderIDs = folderIDs[TYPE];
|
||||||
|
const folderID = typeFolderIDs?.[platform] ?? typeFolderIDs?.default ?? ZEPHYR_FOLDER_ID;
|
||||||
|
|
||||||
|
return folderID;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTestCycle(startDate, endDate) {
|
||||||
|
const {
|
||||||
|
BRANCH,
|
||||||
|
BUILD_ID,
|
||||||
|
JIRA_PROJECT_KEY,
|
||||||
|
ZEPHYR_CYCLE_NAME,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
const testCycle = {
|
||||||
|
projectKey: JIRA_PROJECT_KEY,
|
||||||
|
name: ZEPHYR_CYCLE_NAME ? `${ZEPHYR_CYCLE_NAME} (${BUILD_ID}-${BRANCH})` : `${BUILD_ID}-${BRANCH}`,
|
||||||
|
description: `Playwright automated test with ${BRANCH}`,
|
||||||
|
plannedStartDate: startDate,
|
||||||
|
plannedEndDate: endDate,
|
||||||
|
statusName: 'Done',
|
||||||
|
folderId: await getZEPHYRFolderID(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await saveToEndpoint('https://api.zephyrscale.smartbear.com/v2/testcycles', testCycle);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTestExecutions(report, testCycle) {
|
||||||
|
const {
|
||||||
|
BROWSER,
|
||||||
|
JIRA_PROJECT_KEY,
|
||||||
|
ZEPHYR_ENVIRONMENT_NAME,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
const testCases = getZEPHYRTestCases(report);
|
||||||
|
const startDate = new Date(report.stats.start);
|
||||||
|
const startTime = startDate.getTime();
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
Object.entries(testCases).forEach(([key, steps], index) => {
|
||||||
|
const testScriptResults = steps.
|
||||||
|
sort((a, b) => a.title.localeCompare(b.title)).
|
||||||
|
map((item) => {
|
||||||
|
return {
|
||||||
|
statusName: status[item.state],
|
||||||
|
actualEndDate: new Date(startTime + item.incrementalDuration).toISOString(),
|
||||||
|
actualResult: 'Playwright automated test completed',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const stateResult = getStepStateResult(steps);
|
||||||
|
|
||||||
|
const testExecution = {
|
||||||
|
projectKey: JIRA_PROJECT_KEY,
|
||||||
|
testCaseKey: key,
|
||||||
|
testCycleKey: testCycle.key,
|
||||||
|
statusName: stateResult.passed && stateResult.passed === steps.length ? 'Pass' : 'Fail',
|
||||||
|
testScriptResults,
|
||||||
|
environmentName: ZEPHYR_ENVIRONMENT_NAME || environment[BROWSER] || 'Chrome',
|
||||||
|
actualEndDate: testScriptResults[testScriptResults.length - 1].actualEndDate,
|
||||||
|
executionTime: steps.reduce((acc, prev) => {
|
||||||
|
acc += prev.duration; // eslint-disable-line no-param-reassign
|
||||||
|
return acc;
|
||||||
|
}, 0),
|
||||||
|
comment: `Playwright automated test - ${getStepStateSummary(steps)}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Temporarily log to verify cases that were being saved.
|
||||||
|
console.log(index, key); // eslint-disable-line no-console
|
||||||
|
|
||||||
|
promises.push(saveTestExecution(testExecution, index));
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
console.log('Successfully saved test cases into the Test Management System');
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveTestCases = async (allReport) => {
|
||||||
|
const {start, end} = allReport.stats;
|
||||||
|
|
||||||
|
const testCycle = await createTestCycle(start, end);
|
||||||
|
|
||||||
|
await createTestExecutions(allReport, testCycle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RETRY = [];
|
||||||
|
|
||||||
|
async function saveTestExecution(testExecution, index) {
|
||||||
|
await axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://api.zephyrscale.smartbear.com/v2/testexecutions',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
Authorization: process.env.ZEPHYR_API_KEY,
|
||||||
|
},
|
||||||
|
data: testExecution,
|
||||||
|
}).then(() => {
|
||||||
|
console.log(chalk.green('Success:', index, testExecution.testCaseKey));
|
||||||
|
}).catch((error) => {
|
||||||
|
// Retry on 500 error code / internal server error
|
||||||
|
if (!error.response || error.response.data.errorCode === 500) {
|
||||||
|
if (RETRY[testExecution.testCaseKey]) {
|
||||||
|
RETRY[testExecution.testCaseKey] += 1;
|
||||||
|
} else {
|
||||||
|
RETRY[testExecution.testCaseKey] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTestExecution(testExecution, index);
|
||||||
|
console.log(chalk.magenta('Retry:', index, testExecution.testCaseKey, `(${RETRY[testExecution.testCaseKey]}x)`));
|
||||||
|
} else {
|
||||||
|
console.log(chalk.red('Error:', index, testExecution.testCaseKey, error.response.data.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createTestCycle,
|
||||||
|
saveTestCases,
|
||||||
|
createTestExecutions,
|
||||||
|
};
|
56
e2e/webpack.config.js
Normal file
56
e2e/webpack.config.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const glob = require('glob');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'development',
|
||||||
|
entry: {
|
||||||
|
e2e: glob.sync('./specs/**/*.js'),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist/'),
|
||||||
|
filename: '[name]_bundle.js',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({__IS_MAC_APP_STORE__: false}),
|
||||||
|
],
|
||||||
|
externals: {
|
||||||
|
electron: 'require("electron")',
|
||||||
|
fs: 'require("fs")',
|
||||||
|
ws: 'require("ws")',
|
||||||
|
child_process: 'require("child_process")',
|
||||||
|
dns: 'require("dns")',
|
||||||
|
http2: 'require("http2")',
|
||||||
|
net: 'require("net")',
|
||||||
|
repl: 'require("repl")',
|
||||||
|
tls: 'require("tls")',
|
||||||
|
playwright: 'require("playwright")',
|
||||||
|
robotjs: 'require("robotjs")',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.(js|ts)?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
__filename: false,
|
||||||
|
__dirname: false,
|
||||||
|
},
|
||||||
|
target: 'node',
|
||||||
|
resolve: {
|
||||||
|
modules: [
|
||||||
|
'node_modules',
|
||||||
|
'../src',
|
||||||
|
],
|
||||||
|
alias: {
|
||||||
|
src: path.resolve(__dirname, '../src'),
|
||||||
|
},
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
},
|
||||||
|
};
|
13
e2e/webpack.config.performance.js
Normal file
13
e2e/webpack.config.performance.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
const glob = require('glob');
|
||||||
|
const {merge} = require('webpack-merge');
|
||||||
|
|
||||||
|
const test = require('./webpack.config.test');
|
||||||
|
|
||||||
|
module.exports = merge(test, {
|
||||||
|
entry: {
|
||||||
|
e2e: glob.sync('./e2e/performance/**/*.test.js'),
|
||||||
|
},
|
||||||
|
});
|
190
electron-builder.json
Normal file
190
electron-builder.json
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
{
|
||||||
|
"publish": [
|
||||||
|
{
|
||||||
|
"provider": "generic",
|
||||||
|
"url": "https://gitlab.peanutsmediaserver.com/aaron/mattermost-desktop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"appId": "Mattermost.Desktop",
|
||||||
|
"artifactName": "${version}/${name}-${version}-${os}-${arch}.${ext}",
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "src/assets",
|
||||||
|
"output": "release"
|
||||||
|
},
|
||||||
|
"extraMetadata": {
|
||||||
|
"main": "index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"!node_modules/**/*",
|
||||||
|
"node_modules/bindings/**/*",
|
||||||
|
"node_modules/file-uri-to-path/**/*",
|
||||||
|
"node_modules/macos-notification-state/**/*",
|
||||||
|
"node_modules/windows-focus-assist/**/*",
|
||||||
|
"!**/node_modules/macos-notification-state/bin/**/*",
|
||||||
|
"!**/node_modules/macos-notification-state/build/**/*",
|
||||||
|
"!**/node_modules/windows-focus-assist/bin/**/*",
|
||||||
|
"!**/node_modules/windows-focus-assist/build/**/*",
|
||||||
|
"node_modules/macos-notification-state/build/**/*.node",
|
||||||
|
"node_modules/windows-focus-assist/build/Release/**/*.node",
|
||||||
|
{
|
||||||
|
"from": "dist",
|
||||||
|
"to": ".",
|
||||||
|
"filter": "**/*"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"protocols": [
|
||||||
|
{
|
||||||
|
"name": "Mattermost",
|
||||||
|
"schemes": [
|
||||||
|
"mattermost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"beforePack": "scripts/beforepack.js",
|
||||||
|
"afterPack": "scripts/afterpack.js",
|
||||||
|
"afterAllArtifactBuild": "scripts/afterbuild.js",
|
||||||
|
"deb": {
|
||||||
|
"artifactName": "${version}/${name}_${version}-1_${arch}.${ext}",
|
||||||
|
"synopsis": "Mattermost Desktop App",
|
||||||
|
"depends": [
|
||||||
|
"libnotify4",
|
||||||
|
"libxtst6",
|
||||||
|
"libnss3"
|
||||||
|
],
|
||||||
|
"priority": "optional"
|
||||||
|
},
|
||||||
|
"asarUnpack": [
|
||||||
|
"./node_modules/macos-notification-state/build/Release/**/*.node",
|
||||||
|
"./node_modules/windows-focus-assist/build/Release/**/*.node"
|
||||||
|
],
|
||||||
|
"linux": {
|
||||||
|
"category": "Network;InstantMessaging",
|
||||||
|
"target": [
|
||||||
|
"deb",
|
||||||
|
"tar.gz",
|
||||||
|
"appimage",
|
||||||
|
"rpm"
|
||||||
|
],
|
||||||
|
"extraFiles": [
|
||||||
|
{
|
||||||
|
"filter": [
|
||||||
|
"LICENSE.txt",
|
||||||
|
"NOTICE.txt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "src/assets/linux",
|
||||||
|
"filter": [
|
||||||
|
"create_desktop_file.sh",
|
||||||
|
"app_icon.png",
|
||||||
|
"README.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"appImage": {
|
||||||
|
"executableArgs": [" "]
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"category": "public.app-category.productivity",
|
||||||
|
"target": [
|
||||||
|
"zip",
|
||||||
|
"dmg"
|
||||||
|
],
|
||||||
|
"darkModeSupport": true,
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"filter": [
|
||||||
|
"LICENSE.txt",
|
||||||
|
"NOTICE.txt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"gatekeeperAssess": true,
|
||||||
|
"entitlements": "./resources/mac/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "./resources/mac/entitlements.mac.inherit.plist",
|
||||||
|
"extendInfo": {
|
||||||
|
"NSMicrophoneUsageDescription": "Microphone access may be used by Mattermost plugins, such as Jitsi video conferencing.",
|
||||||
|
"NSCameraUsageDescription": "Camera access may be used by Mattermost plugins, such as Jitsi video conferencing.",
|
||||||
|
"NSFocusStatusUsageDescription": "Focus status is used by Mattermost to determine whether to send notifications or not.",
|
||||||
|
"LSFileQuarantineEnabled": true
|
||||||
|
},
|
||||||
|
"notarize": {
|
||||||
|
"teamId": "UQ8HT4Q2XM"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mas": {
|
||||||
|
"hardenedRuntime": false,
|
||||||
|
"entitlements": "./resources/mac/entitlements.mas.plist",
|
||||||
|
"entitlementsInherit": "./resources/mac/entitlements.mas.inherit.plist",
|
||||||
|
"entitlementsLoginHelper": "./resources/mac/entitlements.mas.inherit.plist",
|
||||||
|
"provisioningProfile": "./mas.provisionprofile",
|
||||||
|
"extendInfo": {
|
||||||
|
"ITSAppUsesNonExemptEncryption": false,
|
||||||
|
"NSUserActivityTypes": ["INSendMessageIntent"]
|
||||||
|
},
|
||||||
|
"singleArchFiles": "*"
|
||||||
|
},
|
||||||
|
"masDev": {
|
||||||
|
"provisioningProfile": "./dev.provisionprofile"
|
||||||
|
},
|
||||||
|
"dmg": {
|
||||||
|
"background": "src/assets/osx/DMG_BG.png",
|
||||||
|
"contents": [
|
||||||
|
{
|
||||||
|
"x": 135,
|
||||||
|
"y": 165
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 407,
|
||||||
|
"y": 165,
|
||||||
|
"type": "link",
|
||||||
|
"path": "/Applications"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"iconSize": 120,
|
||||||
|
"iconTextSize": 14,
|
||||||
|
"window": {
|
||||||
|
"height": 380
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"squirrelWindows": {
|
||||||
|
"iconUrl": "file://src/assets/icon.ico",
|
||||||
|
"artifactName": "${version}/${name}-setup-${version}-${arch}.${ext}"
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
"nsis",
|
||||||
|
"zip",
|
||||||
|
"msi"
|
||||||
|
],
|
||||||
|
"extraFiles": [
|
||||||
|
{
|
||||||
|
"filter": [
|
||||||
|
"LICENSE.txt",
|
||||||
|
"NOTICE.txt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/windows/gpo",
|
||||||
|
"to": "gpo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"signDlls": true,
|
||||||
|
"publisherName": "CN=\"aaron\", O=\"buds inc.\", L=turner somewhere, S=oregon, C=US"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"artifactName": "${version}/${name}-setup-${version}-win.${ext}",
|
||||||
|
"packElevateHelper": false,
|
||||||
|
"uninstallDisplayName": "${productName}",
|
||||||
|
"include": "scripts/installer.nsh",
|
||||||
|
"warningsAsErrors": false
|
||||||
|
},
|
||||||
|
"msi": {
|
||||||
|
"additionalWixArgs": ["-ext", "WixUtilExtension"]
|
||||||
|
},
|
||||||
|
"rpm": {
|
||||||
|
"fpm": ["--rpm-rpmbuild-define", "_build_id_links none"]
|
||||||
|
}
|
||||||
|
}
|
36
fastlane/Fastfile
Normal file
36
fastlane/Fastfile
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
fastlane_version '2.71.0'
|
||||||
|
fastlane_require 'aws-sdk-s3'
|
||||||
|
fastlane_require 'erb'
|
||||||
|
fastlane_require 'json'
|
||||||
|
fastlane_require 'pathname'
|
||||||
|
|
||||||
|
lane :publish_test do |options|
|
||||||
|
api_key = ''
|
||||||
|
unless ENV['MACOS_API_KEY_ID'].nil? || ENV['MACOS_API_KEY_ID'].empty? ||
|
||||||
|
ENV['MACOS_API_ISSUER_ID'].nil? || ENV['MACOS_API_ISSUER_ID'].empty? ||
|
||||||
|
ENV['MACOS_API_KEY'].nil? || ENV['MACOS_API_KEY'].empty?
|
||||||
|
api_key_path = "#{ENV['MACOS_API_KEY_ID']}.p8"
|
||||||
|
File.open("../#{api_key_path}", 'w') do |f|
|
||||||
|
key_string = ENV['MACOS_API_KEY']
|
||||||
|
p8_array = key_string.split('\n')
|
||||||
|
p8_array.each_with_index do |value, index|
|
||||||
|
f.write(value)
|
||||||
|
f.write("\n") unless index == p8_array.length - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
api_key = app_store_connect_api_key(
|
||||||
|
key_id: ENV['MACOS_API_KEY_ID'],
|
||||||
|
issuer_id: ENV['MACOS_API_ISSUER_ID'],
|
||||||
|
key_filepath: "./#{api_key_path}",
|
||||||
|
in_house: ENV['MACOS_IN_HOUSE'] == 'true', # optional but may be required if using match/sigh
|
||||||
|
)
|
||||||
|
|
||||||
|
File.delete("../#{api_key_path}")
|
||||||
|
end
|
||||||
|
pilot(
|
||||||
|
pkg: options[:path],
|
||||||
|
skip_waiting_for_build_processing: ENV['CI'] === 'true',
|
||||||
|
api_key: api_key
|
||||||
|
)
|
||||||
|
end
|
1
i18n/am.json
Normal file
1
i18n/am.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
304
i18n/ar.json
Normal file
304
i18n/ar.json
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
{
|
||||||
|
"common.permissions.canBasicAuth": "مصادقه الويب",
|
||||||
|
"common.tabs.TAB_FOCALBOARD": "اللوحات",
|
||||||
|
"common.tabs.TAB_MESSAGING": "القنوات",
|
||||||
|
"common.tabs.TAB_PLAYBOOKS": "خطط العمل",
|
||||||
|
"label.accept": "قبول",
|
||||||
|
"label.add": "أضف",
|
||||||
|
"label.allow": "سماح",
|
||||||
|
"label.cancel": "إلغاء",
|
||||||
|
"label.change": "تغيير",
|
||||||
|
"label.close": "اغلاق",
|
||||||
|
"label.deny": "رفض",
|
||||||
|
"label.denyPermanently": "رفض بشكل دائم",
|
||||||
|
"label.login": "تسجيل الدخول",
|
||||||
|
"label.no": "لا",
|
||||||
|
"label.ok": "نعم",
|
||||||
|
"label.remove": "إزالة",
|
||||||
|
"label.save": "حفظ",
|
||||||
|
"label.yes": "موافق",
|
||||||
|
"main.CriticalErrorHandler.uncaughtException.button.reopen": "اعاده فتح",
|
||||||
|
"main.CriticalErrorHandler.uncaughtException.button.showDetails": "اظهار التفاصيل",
|
||||||
|
"main.CriticalErrorHandler.uncaughtException.dialog.message": "تم إنهاء التطبيق {appName} بشكل غير متوقع. انقر على \"{showDetails}\" لمعرفة المزيد أو \"{reopen}\" لفتح التطبيق مرة أخرى.\n\nخطأ داخلي: {err}",
|
||||||
|
"main.CriticalErrorHandler.unresponsive.dialog.message": "النافذة لم تعد تستجيب.\nهل تريد الانتظار حتى تستجيب النافذة مرة أخرى؟",
|
||||||
|
"main.allowProtocolDialog.button.saveProtocolAsAllowed": "نعم (احفظ {protocol} على النحو المسموح به)",
|
||||||
|
"main.allowProtocolDialog.detail": "الرابط المطلوب هو {URL}. هل تريد الاستمرار ؟",
|
||||||
|
"main.allowProtocolDialog.message": "{protocol} الرابط يحتاج تطبيق خارجى.",
|
||||||
|
"main.allowProtocolDialog.title": "ليس بروتوكول http(s)",
|
||||||
|
"main.app.app.handleAppCertificateError.certError.button.cancelConnection": "ألغاء الاتصال",
|
||||||
|
"main.app.app.handleAppCertificateError.certError.button.moreDetails": "تفاصيل اكثر",
|
||||||
|
"main.app.app.handleAppCertificateError.certError.dialog.detail": "{extraDetail} الاصل: {origin}\nخطأ: {error}",
|
||||||
|
"main.app.app.handleAppCertificateError.certError.dialog.message": "هناك مشكلة في الاعدادات مع خادم Mattermost هذا ، أو أن شخصًا ما يحاول اعتراض اتصالك. قد تحتاج أيضًا إلى تسجيل الدخول إلى شبكة Wi-Fi التي تتصل بها باستخدام متصفح الويب الخاص بك.",
|
||||||
|
"main.app.app.handleAppCertificateError.certError.dialog.title": "خطأ فى شهادة التصديق",
|
||||||
|
"main.app.app.handleAppCertificateError.certNotTrusted.button.cancelConnection": "إلغاء الاتصال",
|
||||||
|
"main.app.app.handleAppCertificateError.certNotTrusted.button.trustInsecureCertificate": "مصادقة الشهادة غير الآمنة",
|
||||||
|
"main.app.app.handleAppCertificateError.certNotTrusted.dialog.message": "الشهادة من \"{issuerName}\" غير موثقة.",
|
||||||
|
"main.app.app.handleAppCertificateError.certNotTrusted.dialog.title": "الشهادة غير موثقة",
|
||||||
|
"main.app.app.handleAppCertificateError.dialog.extraDetail": "الشهادة مختلفة عن السابقة.\n\n",
|
||||||
|
"main.app.initialize.downloadBox.allFiles": "كل الملفات",
|
||||||
|
"main.app.utils.migrateMacAppStore.button.dontImport": "لا تستورد",
|
||||||
|
"main.app.utils.migrateMacAppStore.button.selectAndImport": "حدد مسار ثم استورد",
|
||||||
|
"main.app.utils.migrateMacAppStore.dialog.detail": "يبدو أن تهيئة {appName} موجودة ، هل ترغب في استيرادها؟ سيُطلب منك اختيار دليل التكوين الصحيح.",
|
||||||
|
"main.app.utils.migrateMacAppStore.dialog.message": "استيراد الاعدادات الحالية",
|
||||||
|
"main.autoUpdater.noUpdate.detail": "أنت تستخدم أحدث إصدار من {appName} نسخة سطح المكتب (الإصدار {version}). سيتم إشعارك عند توفر إصدار جديد للتثبيت.",
|
||||||
|
"main.autoUpdater.noUpdate.message": "لديك اخر اصدار",
|
||||||
|
"main.badge.noUnreads": "ليس لديك رسائل غير مقروءة",
|
||||||
|
"main.badge.sessionExpired": "انتهت الجلسة: الرجاء تسجيل الدخول لمتابعة تلقي الإخطارات.",
|
||||||
|
"main.badge.unreadChannels": "لديك قنوات غير مقروءة",
|
||||||
|
"main.badge.unreadMentions": "لديك ({mentionCount}) إشعار(ات) غير مقروءة",
|
||||||
|
"main.downloadsManager.resetDownloadsFolder": "يرجى إعادة تعيين المجلد الذي سيحتوي الملفات المنزلة",
|
||||||
|
"main.downloadsManager.specifyDownloadsFolder": "المجلد الذي سوف يحتوي الملفات المنزلة",
|
||||||
|
"main.menus.app.edit": "&تحرير",
|
||||||
|
"main.menus.app.edit.copy": "نسخ",
|
||||||
|
"main.menus.app.edit.cut": "قطع",
|
||||||
|
"main.menus.app.edit.paste": "لصق",
|
||||||
|
"main.menus.app.edit.pasteAndMatchStyle": "لصق ومطابقة النمط",
|
||||||
|
"main.menus.app.edit.redo": "إعادة",
|
||||||
|
"main.menus.app.edit.selectAll": "تحديد الكل",
|
||||||
|
"main.menus.app.edit.undo": "تراجع",
|
||||||
|
"main.menus.app.file": "&ملف",
|
||||||
|
"main.menus.app.file.about": "عن {appName}",
|
||||||
|
"main.menus.app.file.exit": "انهاء",
|
||||||
|
"main.menus.app.file.hide": "اخفاء {appName}",
|
||||||
|
"main.menus.app.file.hideOthers": "اخفاء الاخرين",
|
||||||
|
"main.menus.app.file.preferences": "التفضيلات..",
|
||||||
|
"main.menus.app.file.quit": "انهاء {appName}",
|
||||||
|
"main.menus.app.file.settings": "الاعدادات...",
|
||||||
|
"main.menus.app.file.signInToAnotherServer": "تسجيل الدخول لخادم اخر",
|
||||||
|
"main.menus.app.file.unhide": "اظهار الكل",
|
||||||
|
"main.menus.app.help": "المساعدة",
|
||||||
|
"main.menus.app.help.RunDiagnostics": "تشغيل التشخيصات",
|
||||||
|
"main.menus.app.help.ShowLogs": "اظهار السجلات",
|
||||||
|
"main.menus.app.help.checkForUpdates": "تحقق من وجود تحديثات",
|
||||||
|
"main.menus.app.help.commitString": " التعديل: {hashVersion}",
|
||||||
|
"main.menus.app.help.downloadUpdate": "تنزل التحديثات",
|
||||||
|
"main.menus.app.help.learnMore": "اعرف اكثر...",
|
||||||
|
"main.menus.app.help.restartAndUpdate": "اعد التشغيل والتحديث",
|
||||||
|
"main.menus.app.help.versionString": "اصدار{version}{commit}",
|
||||||
|
"main.menus.app.history": "&التاريخ",
|
||||||
|
"main.menus.app.history.back": "الرجوع",
|
||||||
|
"main.menus.app.history.forward": "الامام",
|
||||||
|
"main.menus.app.view": "&عرض",
|
||||||
|
"main.menus.app.view.actualSize": "المقاس الحقيقى",
|
||||||
|
"main.menus.app.view.clearCacheAndReload": "امسح ذاكرة التخزين المؤقت وإعادة التحميل",
|
||||||
|
"main.menus.app.view.devToolsAppWrapper": "أدوات المطور لغلاف التطبيق",
|
||||||
|
"main.menus.app.view.devToolsCurrentCallWidget": "أدوات المطور لعنصر (أداة) الاتصال الاضافي(ة)",
|
||||||
|
"main.menus.app.view.devToolsCurrentServer": "أدوات المطور للخادم الحالي",
|
||||||
|
"main.menus.app.view.devToolsSubMenu": "أدوات المطور",
|
||||||
|
"main.menus.app.view.downloads": "التنزيلات",
|
||||||
|
"main.menus.app.view.find": "بحث..",
|
||||||
|
"main.menus.app.view.fullscreen": "تبديل ملىء الشاشة",
|
||||||
|
"main.menus.app.view.reload": "اعاده تحميل",
|
||||||
|
"main.menus.app.view.toggleDarkMode": "تبديل الوضع المظلم",
|
||||||
|
"main.menus.app.view.zoomIn": "تكبير",
|
||||||
|
"main.menus.app.view.zoomOut": "تصغير",
|
||||||
|
"main.menus.app.window": "&النافذة",
|
||||||
|
"main.menus.app.window.bringAllToFront": "إحضار الكل إلى المقدمة",
|
||||||
|
"main.menus.app.window.close": "اغلاق",
|
||||||
|
"main.menus.app.window.closeWindow": "اغلاق النافذة",
|
||||||
|
"main.menus.app.window.minimize": "تصغير",
|
||||||
|
"main.menus.app.window.selectNextTab": "حدد التبويبه التاليه",
|
||||||
|
"main.menus.app.window.selectPreviousTab": "حدد التبويبة السابقة",
|
||||||
|
"main.menus.app.window.showServers": "اظهار الخوادم",
|
||||||
|
"main.menus.app.window.zoom": "تقريب",
|
||||||
|
"main.menus.tray.preferences": "التفضيلات ...",
|
||||||
|
"main.menus.tray.settings": "الاعدادات...",
|
||||||
|
"main.notifications.download.complete.body": "تم التحميل\n{fileName}",
|
||||||
|
"main.notifications.download.complete.title": "تم التنزيل",
|
||||||
|
"main.notifications.mention.title": "شخص م اشار اليك",
|
||||||
|
"main.notifications.upgrade.newVersion.body": "يتوفر إصدار جديد للتنزيل الآن.",
|
||||||
|
"main.notifications.upgrade.newVersion.title": "إصدار سطح المكتب الجديد متاح",
|
||||||
|
"main.notifications.upgrade.readyToInstall.body": "إصدار سطح المكتب الجديد جاهز للتثبيت الآن.",
|
||||||
|
"main.notifications.upgrade.readyToInstall.title": "انقر لإعادة التشغيل وتثبيت التحديث",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.detail.geolocation": "سيستخدم {appName} الموقع لإعداد منطقتك الزمنية. ويمكنك دائمًا تغيير ذلك لاحقًا في إعدادات جهاز الكمبيوتر الخاص بك.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.detail.media": "{appName} سوف يستخدم المايكروفون و الكاميرا من أجل الاتصالات و الملاحظات الصوتية، يمكنك تغيير هذا لاحقاً من خلال الإعدادات.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.detail.notifications": "{appName} سوف يرسل اشعارات للرسائل والاتصالات. يمكنك ضبط تفضيلات الاشعارات في الإعدادات.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.detail.openExternal": "سيفتح {appName} الرابط المطلوب في تطبيق خارجي. إذا كنت لا تثق بهذا الرابط أو لا تتعرف عليه، فانقر فوق \"رفض\". يمكنك دائمًا تغيير هذا لاحقًا في إعدادات الكمبيوتر.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.detail.screenShare": "سيستخدم {appName} هذا الإذن لمشاركة شاشتك لإجراء المكالمات. يمكنك دائمًا تغيير هذا لاحقًا في إعدادات الكمبيوتر.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.message.geolocation": "{appName} ({url}) يود الوصول الى موقعك.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.message.media": "{appName} ({url}) يود الوصول الى الكاميرا والمايكروفون.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.message.notifications": "{appName} ({url}) يود إرسال الإشعارات.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.message.openExternal": "يريد {appName} ({url}) الحصول على إذن لفتح عنوان URL التالي: {externalURL}",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.message.screenShare": "يرغب {appName} ({url}) في أن يتمكن من عرض شاشتك.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.title": "تم طلب الإذن",
|
||||||
|
"main.tray.tray.expired": "انتهت الجلسة: الرجاء تسجيل الدخول لمتابعة تلقي الإخطارات.",
|
||||||
|
"main.tray.tray.mention": "تم ذِكرك",
|
||||||
|
"main.tray.tray.unread": "لديك قنوات غير مقروءة",
|
||||||
|
"main.views.viewManager.handleDeepLink.error.body": "لا يوجد خادم مهيأ في التطبيق يطابق عنوان url المطلوب: {url}",
|
||||||
|
"main.views.viewManager.handleDeepLink.error.title": "لا يوجد خادم مطابق",
|
||||||
|
"main.windows.mainWindow.closeApp.dialog.checkboxLabel": "لا تسأل مرة أخرى",
|
||||||
|
"main.windows.mainWindow.closeApp.dialog.detail": "لن تتلقى بعد الآن إشعارات بالرسائل. إذا كنت تريد ترك {appName} قيد التشغيل في مصفوفة النظام ، فيمكنك تمكين هذا في الإعدادات.",
|
||||||
|
"main.windows.mainWindow.closeApp.dialog.message": "هل أنت متأكد من أنك تريد الخروج؟",
|
||||||
|
"main.windows.mainWindow.closeApp.dialog.title": "اغلاق التطبيق",
|
||||||
|
"main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel": "لا تظهر مرة أخرى",
|
||||||
|
"main.windows.mainWindow.minimizeToTray.dialog.message": "سيستمر تشغيل {appName} في علبة النظام. يمكن تعطيل هذا في الإعدادات.",
|
||||||
|
"main.windows.mainWindow.minimizeToTray.dialog.title": "تصغير إلى الصينية",
|
||||||
|
"renderer.components.autoSaveIndicator.saved": "تم الحفظ",
|
||||||
|
"renderer.components.autoSaveIndicator.saving": "جاري الحفظ...",
|
||||||
|
"renderer.components.configureServer.cardtitle": "أدخل تفاصيل الخادم الخاص بك",
|
||||||
|
"renderer.components.configureServer.connect.default": "يتصل",
|
||||||
|
"renderer.components.configureServer.connect.override": "الإتصال على أي حال",
|
||||||
|
"renderer.components.configureServer.connect.saving": "جاري الإتصال…",
|
||||||
|
"renderer.components.configureServer.name.info": "الاسم الذي سيظهر في قائمة الخوادم",
|
||||||
|
"renderer.components.configureServer.name.placeholder": "اسم الخادم",
|
||||||
|
"renderer.components.configureServer.subtitle": "بدء اعداد اول خادم للاتصال بـ <br></br> على منصة التواصل مع الفريق",
|
||||||
|
"renderer.components.configureServer.title": "لنتصل بالخادم",
|
||||||
|
"renderer.components.configureServer.url.info": "رابط الخادم الذي يخص Mattermost",
|
||||||
|
"renderer.components.configureServer.url.insecure": "الخادم URL غير امن. لإتصال امن, نأمل استخدام URL مع HTTPS protocol.",
|
||||||
|
"renderer.components.configureServer.url.notMattermost": "لا يبدو أن عنوان URL للخادم المقدم يشير إلى خادم Mattermost صالح. يرجى التحقق من عنوان URL والتحقق من اتصالك.",
|
||||||
|
"renderer.components.configureServer.url.ok": "عنوان URL للخادم صالح. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.configureServer.url.placeholder": "URL الخادم",
|
||||||
|
"renderer.components.configureServer.url.urlNotMatched": "لا يتطابق عنوان URL للخادم المقدم مع عنوان URL للموقع الذي تم تكوينه على خادم Mattermost الخاص بك. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.configureServer.url.urlUpdated": "تم تحديث عنوان URL للخادم المقدم ليتوافق مع عنوان URL للموقع الذي تم تكوينه على خادم Mattermost الخاص بك. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.configureServer.url.validating": "جاري التحقق...",
|
||||||
|
"renderer.components.errorView.cannotConnectToAppName": "لا يمكن الاتصال بـ{appName}",
|
||||||
|
"renderer.components.errorView.havingTroubleConnecting": "نواجه مشكلة في الاتصال بـ {appName} . سنستمر في محاولة إقامة اتصال.",
|
||||||
|
"renderer.components.errorView.refreshThenVerify": "إذا لم يعمل تحديث هذه الصفحة (Ctrl+R أو Command+R)، فيرجى التحقق من ما يلي:",
|
||||||
|
"renderer.components.errorView.troubleshooting.browserView.canReachFromBrowserWindow": "يمكنك الوصول إلى <link>{url}</link> من خلال نافذة متصفح.",
|
||||||
|
"renderer.components.errorView.troubleshooting.computerIsConnected": "جهازك متصل بالانترنت.",
|
||||||
|
"renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect": "إرتباط منصة {appName} <link>{url}</link> صحيح",
|
||||||
|
"renderer.components.extraBar.back": "رجوع",
|
||||||
|
"renderer.components.input.required": "هذه الخانة مطلوبة",
|
||||||
|
"renderer.components.mainPage.contextMenu.ariaLabel": "قائمة الخيارات",
|
||||||
|
"renderer.components.mainPage.titleBar": "{appName}",
|
||||||
|
"renderer.components.newServerModal.error.nameRequired": "الاسم مطلوب.",
|
||||||
|
"renderer.components.newServerModal.error.serverUrlExists": "يوجد بالفعل خادم بنفس عنوان URL.",
|
||||||
|
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL ليس صحيحاً.",
|
||||||
|
"renderer.components.newServerModal.error.urlRequired": "لم يتم ادخال الـURL.",
|
||||||
|
"renderer.components.newServerModal.permissions.geolocation": "الموقع",
|
||||||
|
"renderer.components.newServerModal.permissions.microphoneAndCamera": "المايكروفون والكاميرا",
|
||||||
|
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsCameraPermissions": "تم تعطيل الكاميرا في إعدادات Windows. انقر <link>هنا</link> لفتح إعدادات الكاميرا.",
|
||||||
|
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsMicrophoneaPermissions": "تم تعطيل الميكروفون في إعدادات Windows. انقر <link>هنا</link> لفتح إعدادات الميكروفون.",
|
||||||
|
"renderer.components.newServerModal.permissions.notifications": "التنبيهات",
|
||||||
|
"renderer.components.newServerModal.permissions.notifications.mac": "قد تحتاج أيضًا إلى تمكين الإشعارات في نظام التشغيل macOS لتطبيق Mattermost. انقر <link>هنا</link> لفتح تفضيلات النظام.",
|
||||||
|
"renderer.components.newServerModal.permissions.notifications.windows": "قد تحتاج أيضًا إلى تمكين الإشعارات في Windows لـ Mattermost. انقر <link>هنا</link> لفتح إعدادات الإشعارات.",
|
||||||
|
"renderer.components.newServerModal.permissions.screenShare": "مشاركة الشاشة",
|
||||||
|
"renderer.components.newServerModal.permissions.title": "الصلاحيات",
|
||||||
|
"renderer.components.newServerModal.serverDisplayName": "اسم الخادم",
|
||||||
|
"renderer.components.newServerModal.serverDisplayName.description": "اسم الخادم الى التطبيق.",
|
||||||
|
"renderer.components.newServerModal.serverURL": "URL الخادم",
|
||||||
|
"renderer.components.newServerModal.serverURL.description": "عنوان URL لخادم Mattermost الخاص بك. يجب أن يبدأ بـ http:// أو https://.",
|
||||||
|
"renderer.components.newServerModal.success.ok": "عنوان URL للخادم صالح. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.newServerModal.title.add": "اضافة خادم",
|
||||||
|
"renderer.components.newServerModal.title.edit": "تعديل خادم",
|
||||||
|
"renderer.components.newServerModal.validating": "جاري التحقق...",
|
||||||
|
"renderer.components.newServerModal.warning.insecure": "من المحتمل أن يكون عنوان URL الخاص بخادمك غير آمن. للحصول على أفضل النتائج، استخدم عنوان URL مع بروتوكول HTTPS.",
|
||||||
|
"renderer.components.newServerModal.warning.notMattermost": "لا يبدو أن عنوان URL للخادم المقدم يشير إلى خادم Mattermost صالح. يرجى التحقق من عنوان URL والتحقق من اتصالك.",
|
||||||
|
"renderer.components.newServerModal.warning.urlNotMatched": "لا يتطابق عنوان URL للخادم مع عنوان URL للموقع الذي تم تكوينه على خادم Mattermost الخاص بك. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.newServerModal.warning.urlUpdated": "تم تحديث عنوان URL للخادم المقدم ليتوافق مع عنوان URL للموقع الذي تم تكوينه على خادم Mattermost الخاص بك. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.removeServerModal.body": "سيؤدي هذا إلى إزالة الخادم من تطبيق سطح المكتب الخاص بك ولكن لن يؤدي إلى حذف أي من بياناته - يمكنك إضافة الخادم مرة أخرى إلى التطبيق في أي وقت.",
|
||||||
|
"renderer.components.removeServerModal.confirm": "هل تريد تأكيد رغبتك في إزالة الخادم {serverName}؟",
|
||||||
|
"renderer.components.removeServerModal.title": "إزالة الخادم",
|
||||||
|
"renderer.components.saveButton.save": "حفظ",
|
||||||
|
"renderer.components.saveButton.saving": "جاري الحفظ",
|
||||||
|
"renderer.components.serverDropdownButton.noServersConfigured": "لا توجد خوادم تم تكوينها",
|
||||||
|
"renderer.components.settingsPage.afterRestart": "يسري مفعول الإعداد بعد إعادة تشغيل التطبيق.",
|
||||||
|
"renderer.components.settingsPage.appLanguage": "ضبط لغة التطبيق (اختباري)",
|
||||||
|
"renderer.components.settingsPage.appLanguage.description": "يختار اللغة التي سيستخدمها تطبيق سطح المكتب لعناصر القائمة والنوافذ المنبثقة. لا يزال التطبيق في مرحلة الإصدار التجريبي، وقد تفتقر بعض اللغات إلى سلاسل الترجمة.",
|
||||||
|
"renderer.components.settingsPage.appLanguage.useSystemDefault": "استخدم النظام الافتراضي",
|
||||||
|
"renderer.components.settingsPage.appOptions": "خيارات التطبيق",
|
||||||
|
"renderer.components.settingsPage.bounceIcon": "ارتد أيقونة Dock",
|
||||||
|
"renderer.components.settingsPage.bounceIcon.description": "إذا تم تمكين هذا الخيار، يرتد رمز Dock مرة واحدة أو حتى يفتح المستخدم التطبيق عند تلقي إشعار جديد.",
|
||||||
|
"renderer.components.settingsPage.bounceIcon.once": "مرة",
|
||||||
|
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "حتى أفتح التطبيق",
|
||||||
|
"renderer.components.settingsPage.checkSpelling": "التحقق من الإملاء",
|
||||||
|
"renderer.components.settingsPage.checkSpelling.description": "قم بتمييز الكلمات المكتوبة بشكل خاطئ في رسائلك استنادًا إلى لغة نظامك أو تفضيلات اللغة.",
|
||||||
|
"renderer.components.settingsPage.checkSpelling.editSpellcheckUrl": "استخدم عنوان URL للقاموس البديل",
|
||||||
|
"renderer.components.settingsPage.checkSpelling.preferredLanguages": "اختر اللغة أو اللغات المرغوبة",
|
||||||
|
"renderer.components.settingsPage.checkSpelling.revertToDefault": "العودة إلى الوضع الافتراضي",
|
||||||
|
"renderer.components.settingsPage.checkSpelling.specifyURL": "حدد عنوان URL حيث يمكن استرداد تعريفات القاموس",
|
||||||
|
"renderer.components.settingsPage.downloadLocation": "مكان التنزيل",
|
||||||
|
"renderer.components.settingsPage.downloadLocation.description": "حدد المجلد الذي سيتم تنزيل الملفات فيه.",
|
||||||
|
"renderer.components.settingsPage.enableHardwareAcceleration": "استخدام تسريع أجهزة GPU",
|
||||||
|
"renderer.components.settingsPage.enableHardwareAcceleration.description": "إذا تم تمكينه، فسيتم عرض واجهة المستخدم {appName} بكفاءة أكبر ولكن قد يؤدي ذلك إلى انخفاض الاستقرار لبعض الأنظمة.",
|
||||||
|
"renderer.components.settingsPage.flashWindow": "اشعار بلون لأيقونة شريط المهام عند تلقي رسالة جديدة",
|
||||||
|
"renderer.components.settingsPage.flashWindow.description": "إذا تم تمكين هذا الخيار، فسوف يومض رمز شريط المهام لبضع ثوانٍ عند تلقي رسالة جديدة.",
|
||||||
|
"renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "قد لا تعمل هذه الوظيفة مع جميع مديري النوافذ في Linux.",
|
||||||
|
"renderer.components.settingsPage.flashWindow.description.note": "ملاحظة: ",
|
||||||
|
"renderer.components.settingsPage.fullscreen": "فتح التطبيق في وضع ملء الشاشة",
|
||||||
|
"renderer.components.settingsPage.fullscreen.description": "إذا تم التمكين، فسيتم فتح تطبيق {appName} دائماً في وضع ملء الشاشة",
|
||||||
|
"renderer.components.settingsPage.header": "الاعدادات",
|
||||||
|
"renderer.components.settingsPage.launchAppMinimized": "تشغيل التطبيق المصغر",
|
||||||
|
"renderer.components.settingsPage.launchAppMinimized.description": "إذا تم تمكين هذا الخيار، سيبدأ التطبيق في system tray، ولن يعرض النافذة عند التشغيل.",
|
||||||
|
"renderer.components.settingsPage.loadingConfig": "جاري تحميل التكوين...",
|
||||||
|
"renderer.components.settingsPage.loggingLevel": "مستوى التسجيل",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.description": "يعد التسجيل مفيدًا للمطورين والدعم لعزل المشكلات التي قد تواجهها مع تطبيق سطح المكتب.",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.description.subtitle": "يؤدي زيادة مستوى السجل إلى زيادة استخدام مساحة القرص وقد يؤثر على الأداء. نوصي بزيادة مستوى السجل فقط إذا كنت تواجه مشكلات.",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.debug": "تصحيح الأخطاء (debug)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.error": "الأخطاء (الخطأ)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.info": "معلومات (معلومات)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.silly": "أفضل (سخيف)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.verbose": "مطوّل (مطوّل)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.warn": "الأخطاء والتحذيرات (التحذير)",
|
||||||
|
"renderer.components.settingsPage.minimizeToTray": "ترك التطبيق قيد التشغيل في منطقة الإشعارات عند إغلاق نافذة التطبيق",
|
||||||
|
"renderer.components.settingsPage.minimizeToTray.description": "إذا تم تمكين هذا الخيار، فسيظل التطبيق قيد التشغيل في منطقة الإعلام بعد إغلاق نافذة التطبيق.",
|
||||||
|
"renderer.components.settingsPage.saving.error": "لا يمكن حفظ التغييرات. يرجى المحاولة مرة أخرى.",
|
||||||
|
"renderer.components.settingsPage.showUnreadBadge": "إظهار الشارة الحمراء على أيقونة {taskbar} للإشارة إلى الرسائل غير المقروءة",
|
||||||
|
"renderer.components.settingsPage.showUnreadBadge.description": "بغض النظر عن هذا الإعداد، تتم الإشارة إلى الإشارات دائمًا بشارة حمراء وعدد العناصر على أيقونة {taskbar}.",
|
||||||
|
"renderer.components.settingsPage.startAppOnLogin": "ابدأ تشغيل التطبيق عند تسجيل الدخول",
|
||||||
|
"renderer.components.settingsPage.startAppOnLogin.description": "إذا تم تمكين هذا الخيار، فسيتم تشغيل التطبيق تلقائيًا عند تسجيل الدخول إلى جهازك.",
|
||||||
|
"renderer.components.settingsPage.trayIcon.color": "لون الأيقونة: ",
|
||||||
|
"renderer.components.settingsPage.trayIcon.show": "إظهار الرمز في منطقة الإشعارات",
|
||||||
|
"renderer.components.settingsPage.trayIcon.show.darwin": "إظهار أيقونة {appName} في شريط القائمة",
|
||||||
|
"renderer.components.settingsPage.trayIcon.theme.dark": "داكن",
|
||||||
|
"renderer.components.settingsPage.trayIcon.theme.light": "فاتح",
|
||||||
|
"renderer.components.settingsPage.trayIcon.theme.systemDefault": "استخدم النظام الافتراضي",
|
||||||
|
"renderer.components.settingsPage.updates": "تحديثات",
|
||||||
|
"renderer.components.settingsPage.updates.automatic": "التحقق تلقائيًا من التحديثات",
|
||||||
|
"renderer.components.settingsPage.updates.automatic.description": "إذا تم تمكين هذا الخيار، فسيتم تنزيل التحديثات الخاصة بتطبيق سطح المكتب تلقائيًا وسيتم إعلامك عندما تصبح جاهزًا للتثبيت.",
|
||||||
|
"renderer.components.settingsPage.updates.checkNow": "التحقق من التحديثات الآن",
|
||||||
|
"renderer.components.showCertificateModal.algorithm": "خوارزمية",
|
||||||
|
"renderer.components.showCertificateModal.commonName": "الاسم الشائع",
|
||||||
|
"renderer.components.showCertificateModal.issuerName": "اسم المنشئ",
|
||||||
|
"renderer.components.showCertificateModal.noCertSelected": "لم يتم اختيار الشهادة",
|
||||||
|
"renderer.components.showCertificateModal.notValidAfter": "غير صالح بعد",
|
||||||
|
"renderer.components.showCertificateModal.notValidBefore": "غير صالح قبل",
|
||||||
|
"renderer.components.showCertificateModal.publicKeyInfo": "معلومات المفتاح العام",
|
||||||
|
"renderer.components.showCertificateModal.serialNumber": "الرقم التسلسلي",
|
||||||
|
"renderer.components.showCertificateModal.subjectName": "اسم العنوان",
|
||||||
|
"renderer.components.welcomeScreen.button.getStarted": "ابدأ هنا",
|
||||||
|
"renderer.components.welcomeScreen.slides.calls.subtitle": "عندما لا تكون الكتابة سريعة بما يكفي، يمكنك الانتقال بسلاسة من الدردشة إلى المكالمات الصوتية ومشاركة الشاشة دون الحاجة إلى تبديل الأدوات.",
|
||||||
|
"renderer.components.welcomeScreen.slides.calls.title": "ابدأ مكالمات آمنة على الفور",
|
||||||
|
"renderer.components.welcomeScreen.slides.collaborate.subtitle": "التواصل والتعاون بشكل فعال مع القنوات المستمرة ومشاركة الملفات ومقاطع التعليمات البرمجية وأتمتة سير العمل المصممة خصيصًا للفرق الفنية.",
|
||||||
|
"renderer.components.welcomeScreen.slides.collaborate.title": "التعاون في الوقت الحقيقي",
|
||||||
|
"renderer.components.welcomeScreen.slides.integrate.subtitle": "قم بتنفيذ سير العمل وأتمتته باستخدام تكاملات مرنة ومخصصة مع أدوات تقنية شائعة مثل GitHub وGitLab وServiceNow.",
|
||||||
|
"renderer.components.welcomeScreen.slides.integrate.title": "التكامل مع الأدوات التي تحبها",
|
||||||
|
"renderer.components.welcomeScreen.slides.welcome.subtitle": "Mattermost عبارة عن منصة تعاون مفتوحة المصدر للأعمال المهمة. آمنة ومرنة ومتكاملة مع الأدوات التي تحبها.",
|
||||||
|
"renderer.components.welcomeScreen.slides.welcome.title": "مرحباً",
|
||||||
|
"renderer.downloadsDropdown.ClearAll": "مسح الكل",
|
||||||
|
"renderer.downloadsDropdown.Downloads": "التنزيلات",
|
||||||
|
"renderer.downloadsDropdown.Update.ANewVersionIsAvailableToInstall": "يتوفر إصدار جديد من تطبيق سطح المكتب لـ{appName} (الإصدار {version}) للتثبيت.",
|
||||||
|
"renderer.downloadsDropdown.Update.DownloadUpdate": "تنزيل التحديث",
|
||||||
|
"renderer.downloadsDropdown.Update.MattermostVersionX": "{appName} الإصدار {version}",
|
||||||
|
"renderer.downloadsDropdown.Update.NewDesktopVersionAvailable": "إصدار سطح المكتب الجديد متاح",
|
||||||
|
"renderer.downloadsDropdown.Update.RestartAndUpdate": "إعادة التشغيل والتحديث",
|
||||||
|
"renderer.downloadsDropdown.remaining": "المتبقي",
|
||||||
|
"renderer.downloadsDropdownMenu.CancelDownload": "الغاء التنزيل",
|
||||||
|
"renderer.downloadsDropdownMenu.Clear": "مسح",
|
||||||
|
"renderer.downloadsDropdownMenu.Open": "فتح",
|
||||||
|
"renderer.downloadsDropdownMenu.ShowInFileExplorer": "إظهار في مستكشف الملفات",
|
||||||
|
"renderer.downloadsDropdownMenu.ShowInFileManager": "عرض في مدير الملفات",
|
||||||
|
"renderer.downloadsDropdownMenu.ShowInFinder": "عرض في Finder",
|
||||||
|
"renderer.downloadsDropdownMenu.ShowInFolder": "إظهار في المجلد",
|
||||||
|
"renderer.dropdown.addAServer": "اضافة خادم",
|
||||||
|
"renderer.dropdown.servers": "الخوادم",
|
||||||
|
"renderer.modals.certificate.certificateModal.certInfoButton": "معلومات الشهادة",
|
||||||
|
"renderer.modals.certificate.certificateModal.issuer": "المنشئ",
|
||||||
|
"renderer.modals.certificate.certificateModal.noCertsAvailable": "لا يوجد شهادة متاحة",
|
||||||
|
"renderer.modals.certificate.certificateModal.serial": "تسلسل",
|
||||||
|
"renderer.modals.certificate.certificateModal.subject": "العنوان",
|
||||||
|
"renderer.modals.certificate.certificateModal.subtitle": "حدد شهادة للتحقق من هويتك {url}",
|
||||||
|
"renderer.modals.certificate.certificateModal.title": "اختيار شهادة",
|
||||||
|
"renderer.modals.login.loginModal.message.proxy": "يتطلب الوكيل {host}:{port} اسم مستخدم وكلمة مرور.",
|
||||||
|
"renderer.modals.login.loginModal.message.server": "يتطلب الخادم {url} اسم مستخدم وكلمة مرور.",
|
||||||
|
"renderer.modals.login.loginModal.password": "كلمة المرور",
|
||||||
|
"renderer.modals.login.loginModal.title": "المصادقة مطلوبة",
|
||||||
|
"renderer.modals.login.loginModal.username": "اسم المستخدم",
|
||||||
|
"renderer.modals.permission.permissionModal.body": "يتطلب الموقع الذي لم يتم تضمينه في تكوين خادم Mattermost الخاص بك الوصول إلى {permission}.",
|
||||||
|
"renderer.modals.permission.permissionModal.requestOriginatedFromOrigin": "نشأ هذا الطلب من <link>{origin}</link>",
|
||||||
|
"renderer.modals.permission.permissionModal.title": "{permission} مطلوب",
|
||||||
|
"renderer.modals.permission.permissionModal.unknownOrigin": "مصدر غير معروف",
|
||||||
|
"renderer.time.hours": "ساعات",
|
||||||
|
"renderer.time.mins": "دقائق",
|
||||||
|
"renderer.time.sec": "ثواني"
|
||||||
|
}
|
27
i18n/be.json
Normal file
27
i18n/be.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"main.badge.unreadMentions": "У вас ёсць непрачытаныя згадкі ({mentionCount})",
|
||||||
|
"main.badge.unreadChannels": "У вас ёсць непрачытаныя каналы",
|
||||||
|
"main.app.utils.migrateMacAppStore.button.dontImport": "Не імпартаваць",
|
||||||
|
"main.app.initialize.downloadBox.allFiles": "Усе файлы",
|
||||||
|
"main.app.app.handleAppCertificateError.certNotTrusted.button.cancelConnection": "Адмяніць злучэнне",
|
||||||
|
"main.app.app.handleAppCertificateError.certError.dialog.title": "Памылка сертыфіката",
|
||||||
|
"common.tabs.TAB_PLAYBOOKS": "Playbooks",
|
||||||
|
"renderer.time.mins": "хвілін",
|
||||||
|
"renderer.time.hours": "гадзін",
|
||||||
|
"renderer.time.sec": "сек",
|
||||||
|
"main.app.app.handleAppCertificateError.certError.button.moreDetails": "Больш падрабязна",
|
||||||
|
"main.app.app.handleAppCertificateError.certError.button.cancelConnection": "Адмяніць злучэнне",
|
||||||
|
"label.yes": "Так",
|
||||||
|
"label.save": "Захаваць",
|
||||||
|
"label.remove": "Выдалiць",
|
||||||
|
"label.ok": "ОК",
|
||||||
|
"label.no": "Не",
|
||||||
|
"label.close": "Зачыніць",
|
||||||
|
"label.change": "Змяніць",
|
||||||
|
"label.cancel": "Скасаваць",
|
||||||
|
"label.add": "Дадаць",
|
||||||
|
"label.accept": "Прыняць",
|
||||||
|
"common.tabs.TAB_MESSAGING": "Channels",
|
||||||
|
"common.tabs.TAB_FOCALBOARD": "Boards",
|
||||||
|
"common.permissions.canBasicAuth": "Вэб-аўтэнтыфікацыя"
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue