提交 ad5d53d6 编写于 作者: T Tomas Vik

test: automated testing strategy

Implementing integration and unit tests using mocha and adding two
documents that describe the testing strategy and guide how to write
new tests.
上级 a1f4b099
{
"extends": ["airbnb-base", "prettier"],
"extends": ["airbnb-base", "prettier" ],
"plugins": ["import", "prettier"],
"env": {
"mocha": true
},
"ignorePatterns": [
"node_modules/",
"src/webview/node_modules/",
......@@ -18,5 +21,40 @@
}
],
"prettier/prettier": ["error"]
}
},
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"rules": {
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never",
"tsx": "never"
}
]
},
"parser": "@typescript-eslint/parser",
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".ts"]
}
}
},
"plugins": [
"@typescript-eslint",
"prettier"
]
}
]
}
......@@ -27,17 +27,23 @@ lint_commit:
- if: '$CI_MERGE_REQUEST_IID'
when: always
test-unit:
stage: test
script:
- npm ci
- npm run test-unit
test-integration:
stage: test
variables:
DISPLAY: ':99.0'
script:
- apt-get update
- apt-get install -y xvfb libxtst6 libnss3 libgtk-3-0 libxss1 libasound2 libsecret-1-0
- apt-get install -y xvfb libxtst6 libnss3 libgtk-3-0 libxss1 libasound2 libsecret-1-0 git
- npm ci
- echo $DISPLAY
- /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- npm test
- npm run test-integration
package:
stage: package
......
......@@ -3,7 +3,7 @@
"version": "0.1.0",
"configurations": [
{
"name": "Extension",
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
......@@ -11,14 +11,30 @@
"stopOnEntry": false
},
{
"name": "Extension Tests",
"name": "Unit Tests",
"args": [
"--timeout",
"999999",
"--colors",
"--ignore",
"${workspaceFolder}/src/webview/node_modules/**",
"${workspaceFolder}/src/**/**.test.js"
],
"internalConsoleOptions": "openOnSessionStart",
"program": "${workspaceFolder}/node_modules/.bin/mocha",
"request": "launch",
"type": "node"
},
{
"name": "Integration Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceRoot}",
"--extensionTestsPath=${workspaceRoot}/out/unit_tests/"
"--extensionTestsPath=${workspaceRoot}/test/integration/",
"<paste the last line of `npm run create-test-workspace` output here>"
],
"stopOnEntry": false
}
......
......@@ -89,18 +89,20 @@ You can always use debugger when you are running the extension in development mo
#### Step - 6 : Run tests
To run tests, open terminal within the project folder and run following;
To run tests, open terminal within the project folder and run following:
```bash
npm test
```
See also [how to write automated tests](docs/writing-tests.md).
#### Step - 7 : Run linter
To run linters, open terminal within the project folder and run following;
```bash
npm format # Automatically formats your code using prettier
npm autofix # Automatically formats your code using prettier and fixes eslint errors
npm eslint
```
......
# Automated testing strategy
This document explains what the extension does, defines requirements for the automated test suite, draws a distinction between unit, integration and E2E tests, and defines what tests are we going to write.
For practical information on how to write tests for this extension, see [Writing tests](writing-tests.md).
## What the extension does
The extension's primary goal is to provide the rich GitLab functionality closer to the place where engineers spend a large portion of their time, the editor. The extension integrates the VS Code editor with a GitLab instance. It does that in four main categories.
- Surfaces project issues and merge requests in the [Tree View](https://code.visualstudio.com/api/extension-capabilities/extending-workbench#tree-view) (file explorer on the left)
- Provides an overview for the current branch in the [status bar](https://code.visualstudio.com/api/extension-capabilities/extending-workbench#status-bar-item) - the running pipeline, the associated merge request and the closing issue
- Shows issues and merge request directly in the extension using a [WebView](https://code.visualstudio.com/api/extension-capabilities/extending-workbench#webview)
- Provide a large number of [commands](https://code.visualstudio.com/api/extension-capabilities/common-capabilities#command) that are creating snippets, opening the GitLab website on different pages, validating the `.gitlab_ci.yml` file and more.
## Requirements for the automated test suite
We do have confidence that the current code works because it runs in production for months and most of the outstanding issues are now tracked as [open issues](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues). So the test suite doesn't have to focus on verification as much as on preventing regression.
At the same time, the tests need to enable easy refactoring since the codebase is going to undergo significant changes soon (e.g. TypeScript migration).
The requirements are then in order of importance:
1. Prevent regression
1. Enable easy refactoring
(*See [Sarah Mei - Five Factor Testing](https://madeintandem.com/blog/five-factor-testing/) for more details on test requirements.*)
## How does testing look in VS Code
When looking at the tests from the perspective of the testing pyramid[^1], the VS Code ecosystem offers two levels of granularity.
### Unit tests
Unit tests (usually run by `mocha`) are run directly in the development environment, and they are not dependent on the VS Code editor and its APIs. The tests will fail to execute if they import any code dependent on the `vscode` module.
### Integration tests
These tests are written in `mocha` as well, but they are running within a VS Code instance. These tests have access to a full [VS Code API](https://code.visualstudio.com/api/references/vscode-api). They can use this API to prepare the editor's state for running the tests.
These are the only tests that can run code dependent on the `vscode` module. Because this module is [not installed through npm](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/7bd63cafb794e565dce30005a06ea9d073740388/package.json#L519-524) and it's only available to the extension as a runtime dependency.
Integration tests in VS Code act as another extension that can manipulate the editor. They can require any module from our extension run it and validate the output.
What the integration tests **can't do** is to [validate how UI looks or simulate user clicking](#quick-side-note-on-missing-e2e-tests).
Now we'll see how the VS Code Extension integrates with the editor and outside world. Then we'll have a look at how we can replace/mock these real integration points to achieve the most realistic test environment.
#### Extension in production
```mermaid
graph LR
subgraph inputs
A[Workspace files]
C[Git]
D[GitLab API]
F[User actions]
end
B[extension]
A --> B
C --> B
D --> B
F --> B
B --> E
subgraph output
E[UI changes]
end
```
The extension reads Git repository status using `git` binary, it reads files from the project workspace, it communicates with the GitLab API using `request-promise`, and it receives input from user in the form of commands and clicks on components. The user sees the output displayed in the editor.
#### Extension under integration tests
```mermaid
graph LR
subgraph inputs
A[Temporary workspace files]
C[Temporary git repository]
D("Mocked GitLab API (msw)")
F(Invoking extensions functions)
end
B[extension]
A --> B
C --> B
D --> B
F --> B
B --> E
subgraph output
E(Extension function results)
end
```
*Legend: Round corners mean that the test doesn't match production use exactly.*
##### Temporary git repository and workspace files
When we start integration tests, we create a temporary folder and initialize a git repository in it. This workspace is no different from any other vscode workspace. The tests provide a high level of confidence that the git/workspace integration will work well in production.
##### Mocked GitLab API (msw)
We'll use [`msw`](https://mswjs.io/docs/) to intercept all HTTP(S) requests during testing, and we'll return mocked responses.
##### Testing extension functions
To avoid more complex and flakey UI testing with simulating user clicks and inspecting the DOM, we are going to test the extension at the VS Code API boundary. The following example explains the testing boundary on the [TreeView](https://code.visualstudio.com/api/extension-guides/tree-view) functionality. (See [Workbench documentation](https://code.visualstudio.com/api/extension-capabilities/extending-workbench) to understand the VS Code-specific terms.)
```mermaid
graph TD
A[Register TreeDataProvider] --> B
B[User selects the GitLab icon in Tree View Container] --> C
subgraph integration tests boundary
C["VS Code invokes issuable.getChildren()"] --> D
D[Extension loads configuration and makes calls to GitLab API] --> E
end
E[Extension returns TreeItem nodes] --> F
F[VS Code displays the items in the Tree View]
```
We are making a trade-off. We'll have less confidence in the extension setup. After changing configuration (e.g. the way we register commands), we'll have to test the change manually. In return, we get much simpler, reliable, and faster testing.
### Quick side note on missing E2E tests
The integration tests provide broad coverage of the functionality, and the preferred decision is to end our testing at the integration level. But the integration tests are not giving us confidence in two areas.
#### The extension functionality is correctly "plugged in" to VS Code
A simple example: We can have a well-implemented and tested command, but we didn't register it in the Editor. All tests will pass, but the user can't use the command.
VS Code project itself does have [a few smoke tests](https://github.com/microsoft/vscode/tree/c1f44f4e3e6410b53b74de904562cd507b96aa8c/test/smoke/src/areas) using their custom [driver](https://github.com/microsoft/vscode/blob/c1f44f4e3e6410b53b74de904562cd507b96aa8c/src/vs/platform/driver/node/driver.ts) for interacting with the editor.
If we find that we release bugs caused by incorrect extension configuration, we might implement a few smoke tests and use the node or chrome driver to run tests simulating the user behaviour.
#### Tests don't call real GitLab API
Using a real GitLab instance would have major disadvantages:
- If we set up an E2E test project on gitlab.com, we could run E2E tests in the CI. Communication with the gitlab.com instance would require a real access token, and so contributors without maintainer access wouldn't be able to run the E2E tests suite.
- The external dependency would severely increase testing time and decrease reliability thanks to network communication.
## What tests are we going to write
The extension doesn't contain much business logic that would warrant extensive unit testing. It mainly integrates the editor with GitLab API. The codebase is also quite small (~20 production files) which means that the test speed is not going to be an issue at any granularity.
Based on the [automated testing requirements](#requirements-for-the-automated-test-suite), this document suggests making the vast majority of tests integration tests. Integration tests are going to provide the most confidence in the extension working as expected[^1]. They are going to test more code with fewer tests, and they shouldn't change much as we refactor the extension, because they don't test inner units of the extension. The easier refactoring might come handy if we [replace some libraries](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/merge_requests/54).
### Drawbacks of using integration tests
The main drawback is the error specificity. Especially since we [don't have a good error logging](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues/145), when the test fails, we often end up with just "the integration test result is not as expected" error. That can be frustrating and will only improve with better error handling. Till then, we will often have to result to [debugging](writing-tests.md#debugging-integration-tests). Luckily the debugging of VS Code integration tests is quite easy.
Another drawback is the learning curve for contributors. The VS Code ecosystem doesn't have established testing practices and most of the extensions are not tested at all[^3]. We can expect that it's going to be hard for contributors to write new types of tests. The maintainers will have to create examples of different tests (commands, status bar, tree view) before we can expect contributors to write integration tests for their contributions.
## Manual testing
Until the automated test suite covers the area of extension that the contributor changes, the author of the change must manually test the feature they are changing/adding.
## Conclusion
This strategy is an intimal draft. Since we now only have integration tests for the Tree View, we might alter the strategy a bit when we add tests for status bar and commands. The `webview` is a completely separate project, and it might be enough to unit test it since it doesn't integrate with the `vscode` almost at all.
We neeed to create a mechanism for limiting side effects in the temporary workspace, but thanks to using `git` it should be as simple as running `git clean -fdx` and `git reset --hard`.
[^1]: [Alexey Golub - Unit Testing is Overrated](https://tyrrrz.me/blog/unit-testing-is-overrated)
[^2]: [Ham Vocke - Practical Testing Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)
[^3]: For example: [GitLens](https://github.com/eamodio/vscode-gitlens), [GitHub unofficial extension](https://marketplace.visualstudio.com/items?itemName=KnisterPeter.vscode-github), [VS Code ESLint extension](https://github.com/Microsoft/vscode-eslint)
# Writing tests
This document provides technical details about our automated tests. Please see [Testing Strategy](testing-strategy.md) document to understand why are we testing this way.
## Technology choice
We are using [`mocha`](https://mochajs.org/) as a test runner for both unit and integration tests. We use [`assert`](https://nodejs.org/docs/latest-v12.x/api/assert.html) for assertions. We use [`vscode-test`](https://code.visualstudio.com/api/working-with-extensions/testing-extension#the-test-script) to run integration tests.
## Unit tests `npm run test-unit`
Modules that don't depend on `vscode` module can be unit tested. Unit tests for a module are placed in the same folder. The name of the test file has `.test.js` suffix.
- `src/git/git_remote_parser.js` - production file
- `src/git/git_remote_parser.test.js` - test file
The tests can be debugged by running the "Unit Tests" [Launch configuration].
## Integration tests `npm run test-integration`
Integration tests mocking the GitLab API using the [`msw`](https://mswjs.io/docs/) module. All API calls made by the extension are intercepted by `msw` and handled by `test/integration/mock_server.js`.
A temporary workspace for integration tests is created once before running the test suite by `test/create_tmp_workspace.ts`. In this helper script, we use [`simple-git`](https://github.com/steveukx/git-js) module to initialize git repository.
### Create a new integration test
When creating a new integration test, you need to know how the tested functionality interacts with the rest of the VS Code, filesystem and GitLab API. Please see the [integration strategy](testing-strategy.md#extension-under-integration-tests) to understand the test boundaries.
#### Prepare VS Code dependency
We are now not mocking any part of VS Code. You should be able to set the extension up by calling the [VS Code extension API](https://code.visualstudio.com/api). You might be able to use some of our services directly to set up the extension. Example is setting up the test token by running ```tokenService.setToken('https://gitlab.com', 'abcd-secret');```
#### Prepare Filesystem dependency
If you need additional `git` configuration for your testing, you can set it up either in `test/create_tmp_workspace.ts` (if all tests need it). Or you can use `simple-git` directly in your test set up code (don't forget to reset the config after your test to prevent side effects).
#### Prepare GitLab API dependency
We use [`msw`](https://mswjs.io/docs/) to intercept any requests and return prepared mock responses. When you want to add a new mocked response, do it in the following steps:
1. add temporary `console.log(config)` just before we make the API call (`await request(config)`) in `gitlab_service.js`
1. Run your tests and note down the logged request that the functionality under test makes
1. Mock the request in `mock_server.js`
### Debugging integration tests
For debugging of the integration tests, we first need to create a test workspace (the `npm run test-integration` task doesn't need this step because it does it automatically). We can do that by running ```npm run create-test-workspace``` script. Then we copy the output to `.vscode/launch.json` instead of the placeholder in the "Integration Tests" launch configuration arguments.
Then we can debug the by running the "Integration Tests" [Launch configuration].
[Launch configuration]: https://code.visualstudio.com/docs/editor/debugging#_launch-configurations
此差异已折叠。
......@@ -494,8 +494,11 @@
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"test": "npm run compile && node ./out/runTest.js",
"eslint": "eslint --ext .js .",
"test-unit": "mocha ./src/**/**.test.js",
"test-integration": "npm run compile && node ./out/runTest.js",
"create-test-workspace": "npm run compile && node ./scripts/create_workspace_for_test_debugging.js",
"test": "npm run test-unit && npm run test-integration",
"eslint": "eslint --ext .js --ext .ts .",
"autofix": "eslint --fix . && prettier --write '**/*.{js,json}'",
"publish": "vsce publish",
"webview": "cd src/webview ; npm install ; npm run watch",
......@@ -504,16 +507,22 @@
"devDependencies": {
"@types/mocha": "^7.0.1",
"@types/node": "^13.7.0",
"@types/temp": "^0.8.34",
"@types/vscode": "^1.41.0",
"conventional-changelog-cli": "^2.0.34",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.0",
"eslint-plugin-prettier": "^3.1.2",
"mocha": "^7.0.1",
"msw": "^0.19.5",
"prettier": "^1.19.1",
"rewire": "^4.0.1",
"simple-git": "^2.14.0",
"temp": "^0.9.1",
"typescript": "^3.7.5",
"vsce": "^1.72.0",
"vscode-test": "^1.3.0"
......
#!/usr/bin/env node
// This script creates a temporary workspace that can be used for debugging integration tests
const { default: createTmpWorkspace } = require('../out/create_tmp_workspace');
createTmpWorkspace(false).then(console.log);
const url = require('url');
const getInstancePath = instanceUrl => {
const { pathname } = url.parse(instanceUrl);
if (pathname !== '/') {
// Remove trailing slash if exists
return pathname.replace(/\/$/, '');
}
// Do not return extra slash if no extra path in instance url
return '';
};
const escapeForRegExp = str => {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
};
function parseGitRemote(instanceUrl, remote) {
// Regex to match gitlab potential starting names for ssh remotes.
if (remote.match(`^[a-zA-Z0-9_-]+@`)) {
// Temporarily disable eslint to be able to start enforcing stricter rules
// eslint-disable-next-line no-param-reassign
remote = `ssh://${remote}`;
}
const { protocol, host, pathname } = url.parse(remote);
if (!host || !pathname) {
return null;
}
const pathRegExp = escapeForRegExp(getInstancePath(instanceUrl));
const match = pathname.match(`${pathRegExp}/:?(.+)/(.*?)(?:.git)?$`);
if (!match) {
return null;
}
return [protocol, host, ...match.slice(1, 3)];
}
module.exports = { parseGitRemote };
const assert = require('assert');
const { parseGitRemote } = require('./git_remote_parser');
const parameters = [
{
argument: 'git@gitlab.com:fatihacet/gitlab-vscode-extension.git',
result: ['ssh:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'gitlab-ci@gitlab-mydomain.com:fatihacet/gitlab-vscode-extension.git',
result: ['ssh:', 'gitlab-mydomain.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'ssh://git@gitlab.com:fatihacet/gitlab-vscode-extension.git',
result: ['ssh:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'git://git@gitlab.com:fatihacet/gitlab-vscode-extension.git',
result: ['git:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'http://git@gitlab.com/fatihacet/gitlab-vscode-extension.git',
result: ['http:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'http://gitlab.com/fatihacet/gitlab-vscode-extension.git',
result: ['http:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'https://git@gitlab.com/fatihacet/gitlab-vscode-extension.git',
result: ['https:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'https://gitlab.com/fatihacet/gitlab-vscode-extension.git',
result: ['https:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'https://gitlab.com/fatihacet/gitlab-vscode-extension',
result: ['https:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'https://gitlab.company.com/fatihacet/gitlab-vscode-extension.git',
result: ['https:', 'gitlab.company.com', 'fatihacet', 'gitlab-vscode-extension'],
},
{
argument: 'https://gitlab.company.com:8443/fatihacet/gitlab-vscode-extension.git',
result: ['https:', 'gitlab.company.com:8443', 'fatihacet', 'gitlab-vscode-extension'],
},
];
describe('git_remote_parser', () => {
parameters.forEach(p => {
it(`should parse ${p.argument}`, () => {
assert.deepEqual(parseGitRemote('https://gitlab.com', p.argument), p.result);
});
});
});
const vscode = require('vscode');
const execa = require('execa');
const url = require('url');
const { parseGitRemote } = require('./git/git_remote_parser');
const currentInstanceUrl = () => vscode.workspace.getConfiguration('gitlab').instanceUrl;
......@@ -64,44 +64,6 @@ async function fetchLastCommitId(workspaceFolder) {
return output;
}
const getInstancePath = () => {
const { pathname } = url.parse(currentInstanceUrl());
if (pathname !== '/') {
// Remove trailing slash if exists
return pathname.replace(/\/$/, '');
}
// Do not return extra slash if no extra path in instance url
return '';
};
const escapeForRegExp = str => {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
};
const parseGitRemote = remote => {
// Regex to match gitlab potential starting names for ssh remotes.
if (remote.match(`^[a-zA-Z0-9_-]+@`)) {
// Temporarily disable eslint to be able to start enforcing stricter rules
// eslint-disable-next-line no-param-reassign
remote = `ssh://${remote}`;
}
const { protocol, host, pathname } = url.parse(remote);
if (!host || !pathname) {
return null;
}
const pathRegExp = escapeForRegExp(getInstancePath());
const match = pathname.match(`${pathRegExp}/:?(.+)/(.*?)(?:.git)?$`);
if (!match) {
return null;
}
return [protocol, host, ...match.slice(1, 3)];
};
async function fetchRemoteUrl(remoteName, workspaceFolder) {
// If remote name isn't provided, we the command returns default remote for the current branch
const getUrlForRemoteName = async name =>
......@@ -119,7 +81,7 @@ async function fetchRemoteUrl(remoteName, workspaceFolder) {
}
if (remoteUrl) {
const [schema, host, namespace, project] = parseGitRemote(remoteUrl);
const [schema, host, namespace, project] = parseGitRemote(currentInstanceUrl(), remoteUrl);
return { schema, host, namespace, project };
}
......
......@@ -33,7 +33,9 @@ async function fetch(path, method = 'GET', data = null) {
err = `${err} You have configured tokens for ${tokens}.`;
}
return vscode.window.showInformationMessage(err);
vscode.window.showInformationMessage(err);
console.error(err);
throw err;
}
const config = {
......@@ -114,6 +116,7 @@ async function fetchCurrentProject(workspaceFolder) {
return await fetchProjectData(remote);
} catch (e) {
console.error(e);
return null;
}
}
......
import * as temp from 'temp';
import simpleGit from 'simple-git';
import * as path from 'path';
import * as fs from 'fs';
const vsCodeSettings = {
'gitlab.instanceUrl': 'https://test.gitlab.com',
};
async function createTempFolder(): Promise<string> {
return new Promise<string>((resolve, reject) => {
temp.mkdir('vscodeWorkplace', (err: Error, dirPath: string) => {
if (err) reject(err);
resolve(dirPath);
});
});
}
async function addFile(folderPath: string, relativePath: string, content: string): Promise<void> {
const fullPath = path.join(folderPath, relativePath);
const dir = path.dirname(fullPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
fs.writeFileSync(fullPath, content);
}
// `autoCleanUp = true` means that the directory gets deleted on process exit
export default async function createTmpWorkspace(autoCleanUp = true): Promise<string> {
if (autoCleanUp) temp.track();
const dirPath = await createTempFolder();
const git = simpleGit(dirPath, { binary: 'git' });
await git.init();
await git.addRemote('origin', 'git@test.gitlab.com:gitlab-org/gitlab.git');
await addFile(dirPath, '/.vscode/settings.json', JSON.stringify(vsCodeSettings));
return dirPath;
}
{
"_links": {
"award_emoji": "https://gitlab.com/api/v4/projects/278964/issues/219925/award_emoji",
"notes": "https://gitlab.com/api/v4/projects/278964/issues/219925/notes",
"project": "https://gitlab.com/api/v4/projects/278964",
"self": "https://gitlab.com/api/v4/projects/278964/issues/219925"
},
"assignee": {
"avatar_url": "https://secure.gravatar.com/avatar/6042a9152ada74d9fb6a0cdce895337e?s=80&d=identicon",
"id": 3457201,
"name": "Tomas Vik",
"state": "active",
"username": "viktomas",
"web_url": "https://gitlab.com/viktomas"
},
"assignees": [
{
"avatar_url": "https://secure.gravatar.com/avatar/6042a9152ada74d9fb6a0cdce895337e?s=80&d=identicon",
"id": 3457201,
"name": "Tomas Vik",
"state": "active",
"username": "viktomas",
"web_url": "https://gitlab.com/viktomas"
}
],
"author": {
"avatar_url": "https://secure.gravatar.com/avatar/2c5d6a63b41cbeb3ea4cccda82e758e1?s=80&d=identicon",
"id": 2935693,
"name": "Kai Armstrong",
"state": "active",
"username": "phikai",
"web_url": "https://gitlab.com/phikai"
},
"blocking_issues_count": null,
"closed_at": null,
"closed_by": null,
"confidential": false,
"created_at": "2020-06-01T17:50:22.893Z",
"description": "## Problem to Solve\n\nThe Web IDE is a more complete editing experience that helps to facilitate workflows across multiple files and merge requests. However, users favor the single file editing experience.\n\n## Additional Details\n\nThis is a test that should be setup behind a feature flag to see what kind/if any feedback is generated by making this change.\n\n## Proposal\n\nThe primary and inverted buttons of `Edit` and `Web IDE` should be switched so that Web IDE more clearly looks like a primary action that users should perform.\n\n![Screenshot_2020-06-01_12.42.05](/uploads/31537a7d4f9bca62e7e9ff2c7bafc4ef/Screenshot_2020-06-01_12.42.05.png)\n\n### Feature Flag\n\nThis feature needs to be done with a feature flag and it would be good to have the ability to either assign groups to the feature or assign individual users depending on what we want to do for testing.\n\nWe **SHOULD NOT** enable this by default in the %13.2 release, but rather at a minimum toggle the feature flag on for `gitlab-org` and `gitlab-com`.\n\n[Feature flag implementation documentation](https://docs.gitlab.com/ee/development/feature_flags/)\n\n#### Feature Flag implemenation\n\n!35957 Introduces `web_ide_primary_edit` feature flag that can be anabled for a group.\n\n### Instrumentation\n\nIt would be good as part of this to add telemetry to these buttons to see how many clicks each button receives: https://docs.gitlab.com/ee/development/telemetry/snowplow.html#implementing-snowplow-js-frontend-tracking\n\nThere is also a basic A/B test process documented here: https://docs.gitlab.com/ee/development/experiment_guide/#how-to-create-an-ab-test\n\n#### Tracking implementation\n\nClicking the edit buttons will trigger the following events.\n\n| | event | label | property |\n| ------ | ------ | --- | --- |\n| ![Screenshot_2020-07-08_at_3.09.59_PM](/uploads/84d8a0d045617281eeaaa60dd8cbd676/Screenshot_2020-07-08_at_3.09.59_PM.png) | `click_edit` | `Edit` | |\n| ![Screenshot_2020-07-08_at_3.11.14_PM](/uploads/bbac329ceca6da11c39f7707bb975921/Screenshot_2020-07-08_at_3.11.14_PM.png) | `click_edit` | `Edit` | `secondary` |\n| ![Screenshot_2020-07-08_at_3.11.19_PM](/uploads/ff23cbccbb002e6f9127cb3ddcbd4f36/Screenshot_2020-07-08_at_3.11.19_PM.png) | `click_edit_ide` | `Web IDE` | |\n| ![Screenshot_2020-07-08_at_3.10.13_PM](/uploads/ed647b6f0720bce60b1494908cbc735a/Screenshot_2020-07-08_at_3.10.13_PM.png) | `click_edit_ide` | `Web IDE` | `secondary` |",
"discussion_locked": null,
"downvotes": 0,
"due_date": null,
"epic": {
"group_id": 9970,
"human_readable_end_date": "Aug 17, 2020",
"human_readable_timestamp": "<strong>25</strong> days remaining",
"id": 61962,
"iid": 3489,
"title": "Increase Web IDE Usage over Single File",
"url": "/groups/gitlab-org/-/epics/3489"
},
"epic_iid": 3489,
"has_tasks": false,
"id": 35284557,
"iid": 219925,
"labels": [
"Category:Web IDE",
"Deliverable",
"backstage [DEPRECATED]",
"devops::create",
"feature flag",
"feature::maintenance",
"frontend",
"group::editor",
"missed-deliverable",
"missed:13.2",
"telemetry",
"workflow::verification"
],
"merge_requests_count": 1,
"milestone": {
"created_at": "2020-04-09T17:39:21.090Z",
"description": "https://about.gitlab.com/releases/",
"due_date": "2020-08-17",
"group_id": 9970,
"id": 1233752,
"iid": 50,
"start_date": "2020-07-18",
"state": "active",
"title": "13.3",
"updated_at": "2020-07-17T11:45:59.705Z",
"web_url": "https://gitlab.com/groups/gitlab-org/-/milestones/50"
},
"moved_to_id": null,
"project_id": 278964,
"references": {
"full": "gitlab-org/gitlab#219925",
"relative": "#219925",
"short": "#219925"
},
"state": "opened",
"task_completion_status": {
"completed_count": 0,
"count": 0
},
"time_stats": {
"human_time_estimate": null,
"human_total_time_spent": null,
"time_estimate": 0,
"total_time_spent": 0
},
"title": "Change primary button for editing on files",
"updated_at": "2020-07-21T14:26:09.029Z",
"upvotes": 0,
"user_notes_count": 8,
"web_url": "https://gitlab.com/gitlab-org/gitlab/-/issues/219925",
"weight": 3
}
{
"approvals_before_merge": null,
"assignee": {
"avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/411701/avatar.png",
"id": 411701,
"name": "Kushal Pandya",
"state": "active",
"username": "kushalpandya",
"web_url": "https://gitlab.com/kushalpandya"
},
"assignees": [
{
"avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/411701/avatar.png",
"id": 411701,
"name": "Kushal Pandya",
"state": "active",
"username": "kushalpandya",
"web_url": "https://gitlab.com/kushalpandya"
},
{
"avatar_url": "https://secure.gravatar.com/avatar/6042a9152ada74d9fb6a0cdce895337e?s=80&d=identicon",
"id": 3457201,
"name": "Tomas Vik",
"state": "active",
"username": "viktomas",
"web_url": "https://gitlab.com/viktomas"
}
],
"author": {
"avatar_url": "https://secure.gravatar.com/avatar/6042a9152ada74d9fb6a0cdce895337e?s=80&d=identicon",
"id": 3457201,
"name": "Tomas Vik",
"state": "active",
"username": "viktomas",
"web_url": "https://gitlab.com/viktomas"
},
"blocking_discussions_resolved": true,
"closed_at": null,
"closed_by": null,
"created_at": "2020-06-04T08:27:29.079Z",
"description": "## What does this MR do?\r\n\r\nRemoving unused action `closeAllFiles` and unused mapped actions `stageChange` and `stageAllChanges` in two separate commits.\r\n\r\nI found this dead code when walking through the Web IDE codebase, this change is not related to any feature currently being implemented.\r\n\r\nThis MR is my first GitLab frontend MR and I'd like to see the MR process in action on a small scale.\r\n\r\n## Does this MR meet the acceptance criteria?\r\n\r\n### Conformity\r\n\r\n- [-] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) \r\n- [-] [Documentation](https://docs.gitlab.com/ee/development/documentation/workflow.html) ([if required](https://docs.gitlab.com/ee/development/documentation/workflow.html#when-documentation-is-required))\r\n- [x] [Code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)\r\n- [-] [Merge request performance guidelines](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)\r\n- [x] [Style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/development/contributing/style_guides.md)\r\n- [ ] [Database guides](https://docs.gitlab.com/ee/development/README.html#database-guides)\r\n- [ ] [Separation of EE specific content](https://docs.gitlab.com/ee/development/ee_features.html#separation-of-ee-code)\r\n\r\n### Availability and Testing\r\n\r\nI tested locally that closing files, discarding changes and committing changes works (only as a sanity check). I wasn't able to find any usages in the codebase and so I'm not expecting any additional risks.\r\n\r\n<!-- What risks does this change pose? How might it affect the quality/performance of the product?\r\nWhat additional test coverage or changes to tests will be needed?\r\nWill it require cross-browser testing?\r\nSee the test engineering process for further guidelines: https://about.gitlab.com/handbook/engineering/quality/test-engineering/ -->\r\n\r\n<!-- If cross-browser testing is not required, please remove the relevant item, or mark it as not needed: [-] -->\r\n\r\n- [-] [Review and add/update tests for this feature/bug](https://docs.gitlab.com/ee/development/testing_guide/index.html). Consider [all test levels](https://docs.gitlab.com/ee/development/testing_guide/testing_levels.html). See the [Test Planning Process](https://about.gitlab.com/handbook/engineering/quality/test-engineering).\r\n- [-] [Tested in all supported browsers](https://docs.gitlab.com/ee/install/requirements.html#supported-web-browsers)\r\n- [-] Informed Infrastructure department of a default or new setting change, if applicable per [definition of done](https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#definition-of-done)\r\n\r\n### Security\r\n\r\nDoes not apply",
"discussion_locked": null,
"downvotes": 0,
"force_remove_source_branch": true,
"has_conflicts": false,
"id": 60609203,
"iid": 33824,
"labels": [
"Category:Web IDE",
"backstage [DEPRECATED]",
"devops::create",
"group::editor",
"workflow::production"
],
"merge_commit_sha": "0e8124ce4ebad018fe6744c6e35f32d17ac74f5d",
"merge_status": "can_be_merged",
"merge_when_pipeline_succeeds": false,
"merged_at": null,
"milestone": {
"created_at": "2020-01-03T12:28:30.160Z",
"description": "",
"due_date": "2020-06-17",
"group_id": 9970,
"id": 1112145,
"iid": 47,
"start_date": "2020-05-18",
"state": "active",
"title": "13.1",
"updated_at": "2020-03-17T17:32:54.882Z",
"web_url": "https://gitlab.com/groups/gitlab-org/-/milestones/47"
},
"project_id": 278964,
"reference": "!33824",
"references": {
"full": "gitlab-org/gitlab!33824",
"relative": "!33824",
"short": "!33824"
},
"sha": "78f72fe3f52f353b3957fa24bc27bcd88ce586bf",
"should_remove_source_branch": null,
"source_branch": "web-ide-remove-dead-code",
"source_project_id": 278964,
"squash": true,
"squash_commit_sha": null,
"state": "opened",
"target_branch": "master",
"target_project_id": 278964,
"task_completion_status": {
"completed_count": 2,
"count": 4
},
"time_stats": {
"human_time_estimate": null,
"human_total_time_spent": null,
"time_estimate": 0,
"total_time_spent": 0
},
"title": "Web IDE - remove unused actions (mappings)",
"updated_at": "2020-06-22T07:52:03.398Z",
"upvotes": 0,
"user_notes_count": 7,
"web_url": "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33824",
"work_in_progress": false
}
{
"avatar_url": "https://assets.gitlab-static.net/uploads/-/system/project/avatar/278964/logo-extra-whitespace.png",
"created_at": "2015-05-20T10:47:11.949Z",
"default_branch": "master",
"description": "GitLab is an open source end-to-end software development platform with built-in version control, issue tracking, code review, CI/CD, and more. Self-host GitLab on your own servers, in a container, or on a cloud provider.",
"forks_count": 2193,
"http_url_to_repo": "https://gitlab.com/gitlab-org/gitlab.git",
"id": 278964,
"last_activity_at": "2020-07-23T12:59:24.905Z",
"name": "GitLab",
"name_with_namespace": "GitLab.org / GitLab",
"namespace": {
"avatar_url": "/uploads/-/system/group/avatar/9970/logo-extra-whitespace.png",
"full_path": "gitlab-org",
"id": 9970,
"kind": "group",
"name": "GitLab.org",
"parent_id": null,
"path": "gitlab-org",
"web_url": "https://gitlab.com/groups/gitlab-org"
},
"path": "gitlab",
"path_with_namespace": "gitlab-org/gitlab",
"readme_url": "https://gitlab.com/gitlab-org/gitlab/-/blob/master/README.md",
"ssh_url_to_repo": "git@gitlab.com:gitlab-org/gitlab.git",
"star_count": 1974,
"tag_list": [],
"web_url": "https://gitlab.com/gitlab-org/gitlab"
}
{
"revision": "8170b984763",
"version": "13.2.0-pre"
}
const path = require('path');
const Mocha = require('mocha');
// glob is available in the VS Code runtime
// eslint-disable-next-line import/no-extraneous-dependencies
const glob = require('glob');
const getAllTestFiles = testsRoot =>
new Promise((resolve, reject) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) reject(err);
resolve(files);
});
});
// This function is a public interface that VS Code uses to run the tests
// eslint-disable-next-line import/prefer-default-export
async function run(testsRoot) {
// Create the mocha test
const mocha = new Mocha();
mocha.timeout(2000);
mocha.color(true);
const files = await getAllTestFiles(testsRoot);
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
// Run the mocha test
await new Promise((res, rej) =>
mocha.run(failures => {
if (failures) {
rej(failures);
} else {
res();
}
}),
);
}
module.exports = { run };
module.exports = {
GITLAB_HOST: 'test.gitlab.com',
};
const { setupServer } = require('msw/node');
const { rest } = require('msw');
const { GITLAB_HOST } = require('./constants');
const projectResponse = require('../fixtures/project.json');
const versionResponse = require('../fixtures/version.json');
const openIssueResponse = require('../fixtures/open_issue.json');
const openMergeRequestResponse = require('../fixtures/open_mr.json');
const instancePrefix = `https://${GITLAB_HOST}/api/v4`;
const createEndpoint = (path, response) =>
rest.get(`${instancePrefix}${path}`, (req, res, ctx) => {
return res(ctx.status(200), ctx.json(response));
});
const notFoundByDefault = rest.get(/.*/, (req, res, ctx) => res(ctx.status(404)));
module.exports = () =>
setupServer(
createEndpoint('/projects/gitlab-org%2Fgitlab', projectResponse),
createEndpoint('/version', versionResponse),
createEndpoint('/projects/278964/merge_requests?scope=assigned_to_me&state=opened', [
openMergeRequestResponse,
]),
createEndpoint('/projects/278964/issues?scope=assigned_to_me&state=opened', [
openIssueResponse,
]),
notFoundByDefault,
);
const assert = require('assert');
const IssuableDataProvider = require('../../src/data_providers/issuable').DataProvider;
const tokenService = require('../../src/token_service');
const getServer = require('./test_infrastructure/mock_server');
const { GITLAB_HOST } = require('./test_infrastructure/constants');
describe('GitLab tree view', () => {
let server;
let dataProvider;
before(() => {
server = getServer();
server.listen({ onUnhandledRequest: 'error' }); // TODO this behaviour is going to be supported in the next msw release
tokenService.setToken(`https://${GITLAB_HOST}`, 'abcd-secret');
});
beforeEach(() => {
server.resetHandlers();
dataProvider = new IssuableDataProvider();
});
after(() => {
server.close();
});
/**
* Opens a top level category from the extension issues tree view
*/
async function openCategory(label) {
const categories = await dataProvider.getChildren();
const [chosenCategory] = categories.filter(c => c.label === label);
assert(
chosenCategory,
`Can't open category ${label} because it's not present in ${categories}`,
);
return await dataProvider.getChildren(chosenCategory);
}
it('shows project issues assigned to me', async () => {
const issuesAssignedToMe = await openCategory('Issues assigned to me');
assert.strictEqual(issuesAssignedToMe.length, 1);
assert.strictEqual(
issuesAssignedToMe[0].label,
'#219925 · Change primary button for editing on files',
);
});
it('shows project merge requests assigned to me', async () => {
const mergeRequestsAssignedToMe = await openCategory('Merge requests assigned to me');
assert.strictEqual(mergeRequestsAssignedToMe.length, 1);
assert.strictEqual(
mergeRequestsAssignedToMe[0].label,
'!33824 · Web IDE - remove unused actions (mappings)',
);
});
});
import * as path from 'path';
import { runTests } from 'vscode-test';
import createTmpWorkspace from './create_tmp_workspace';
async function go() {
try {
const extensionDevelopmentPath = path.resolve(__dirname, '../');
const extensionTestsPath = path.resolve(__dirname, './unit_tests');
const extensionTestsPath = path.resolve(__dirname, '../test/integration');
const temporaryWorkspace = await createTmpWorkspace();
console.log(temporaryWorkspace);
await runTests({
extensionDevelopmentPath,
extensionTestsPath,
launchArgs: ['--disable-extensions'],
launchArgs: ['--disable-extensions', temporaryWorkspace],
});
} catch (err) {
console.error('Failed to run tests');
console.error('Failed to run tests', err);
process.exit(1);
}
}
......
import * as assert from 'assert';
import { before } from 'mocha';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../extension';
suite('Extension Test Suite 1', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.equal(-1, [1, 2, 3].indexOf(5));
assert.equal(-1, [1, 2, 3].indexOf(0));
});
});
import * as assert from 'assert';
import * as vscode from 'vscode';
import * as rewire from 'rewire';
const gitService = rewire('../../src/git_service');
suite('git_service tests', () => {
vscode.window.showInformationMessage('Start all tests.');
test('parseGitRemote', () => {
const parseGitRemote = gitService.__get__('parseGitRemote');
assert.deepEqual(parseGitRemote('git@gitlab.com:fatihacet/gitlab-vscode-extension.git'), [
'ssh:',
'gitlab.com',
'fatihacet',
'gitlab-vscode-extension',
]);
assert.deepEqual(parseGitRemote('gitlab-ci@gitlab-mydomain.com:fatihacet/gitlab-vscode-extension.git'), [
'ssh:',
'gitlab-mydomain.com',
'fatihacet',
'gitlab-vscode-extension',
]);
assert.deepEqual(parseGitRemote('ssh://git@gitlab.com:fatihacet/gitlab-vscode-extension.git'), [
'ssh:',
'gitlab.com',
'fatihacet',
'gitlab-vscode-extension',
]);
assert.deepEqual(parseGitRemote('git://git@gitlab.com:fatihacet/gitlab-vscode-extension.git'), [
'git:',
'gitlab.com',
'fatihacet',
'gitlab-vscode-extension',
]);
assert.deepEqual(
parseGitRemote('http://git@gitlab.com/fatihacet/gitlab-vscode-extension.git'),
['http:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
);
assert.deepEqual(parseGitRemote('http://gitlab.com/fatihacet/gitlab-vscode-extension.git'), [
'http:',
'gitlab.com',
'fatihacet',
'gitlab-vscode-extension',
]);
assert.deepEqual(
parseGitRemote('https://git@gitlab.com/fatihacet/gitlab-vscode-extension.git'),
['https:', 'gitlab.com', 'fatihacet', 'gitlab-vscode-extension'],
);
assert.deepEqual(parseGitRemote('https://gitlab.com/fatihacet/gitlab-vscode-extension.git'), [
'https:',
'gitlab.com',
'fatihacet',
'gitlab-vscode-extension',
]);
assert.deepEqual(parseGitRemote('https://gitlab.com/fatihacet/gitlab-vscode-extension'), [
'https:',
'gitlab.com',
'fatihacet',
'gitlab-vscode-extension',
]);
assert.deepEqual(
parseGitRemote('https://gitlab.company.com/fatihacet/gitlab-vscode-extension.git'),
['https:', 'gitlab.company.com', 'fatihacet', 'gitlab-vscode-extension'],
);
assert.deepEqual(
parseGitRemote('https://gitlab.company.com:8443/fatihacet/gitlab-vscode-extension.git'),
['https:', 'gitlab.company.com:8443', 'fatihacet', 'gitlab-vscode-extension'],
);
});
});
import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
export function run(testsRoot: string, cb: (error: any, failures?: number) => void): void {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
});
mocha.useColors(true);
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return cb(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
cb(null, failures);
});
} catch (err) {
console.error(err);
cb(err);
}
});
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册