Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ npm-debug.log*
flow/logs/*log*

# Runtime data
docker-container-id.txt
pids
*.pid
*.seed
Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ before_script:
- if [ $GUI ]; then export DISPLAY=:99.0 && sh -e /etc/init.d/xvfb start && sleep 3; fi
script:
- if [[ $TOXENV == 'discopane-ui-tests' ]]; then
docker run --cidfile=./docker-container.txt -d -p 4000:4000 -e NODE_APP_INSTANCE=disco -e NODE_ENV=uitests $(docker build -q .) /bin/sh -c "npm run build && npm run start" && sleep 60;
yarn start-func-test-server;
fi
- if [ $TOXENV ]; then tox; fi
- if [ $TASK ]; then yarn $TASK; fi
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,24 @@ The easiest way to manage multiple node versions in development is to use

Here are some commands you can run:

| Command | Description |
|---------------------------|-------------|
| yarn dev:amo | Start the dev server and proxy (amo) |
| yarn dev:amo:no-proxy | Start the dev server without proxy (amo) |
| yarn dev:disco | Start the dev server (discovery pane) |
| yarn flow | Run Flow. By default this checks for errors and exits |
| yarn flow:check | Explicitly check for Flow errors and exit |
| yarn flow:dev | Continuously check for Flow errors |
| yarn eslint | Lint the JS |
| yarn stylelint | Lint the SCSS |
| yarn lint | Run all the JS + SCSS linters |
| yarn version-check | Check you have the required dependencies |
| yarn test | Run all tests (Enters [jest][] in `--watch` mode) |
| yarn test-coverage | Run all tests and generate code coverage report (Enters [jest][] in `--watch` mode) |
| yarn test-coverage-once | Run all tests, generate code coverage report, then exit |
| yarn test-once | Run all tests, run all JS + SCSS linters, then exit |
| yarn test-ci | Run all continuous integration checks. This is only meant to run on TravisCI. |
| Command | Description |
|-----------------------------|-------------|
| yarn dev:amo | Start the dev server and proxy (amo) |
| yarn dev:amo:no-proxy | Start the dev server without proxy (amo) |
| yarn dev:disco | Start the dev server (discovery pane) |
| yarn flow | Run Flow. By default this checks for errors and exits |
| yarn flow:check | Explicitly check for Flow errors and exit |
| yarn flow:dev | Continuously check for Flow errors |
| yarn eslint | Lint the JS |
| yarn start-func-test-server | Start a Docker container for functional tests |
| yarn stylelint | Lint the SCSS |
| yarn lint | Run all the JS + SCSS linters |
| yarn version-check | Check you have the required dependencies |
| yarn test | Run all tests (Enters [jest][] in `--watch` mode) |
| yarn test-coverage | Run all tests and generate code coverage report (Enters [jest][] in `--watch` mode) |
| yarn test-coverage-once | Run all tests, generate code coverage report, then exit |
| yarn test-once | Run all tests, run all JS + SCSS linters, then exit |
| yarn test-ci | Run all continuous integration checks. This is only meant to run on TravisCI. |

### Running tests

Expand Down
191 changes: 191 additions & 0 deletions bin/start-func-test-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/* @flow */
/* eslint-disable no-console */
// This starts a docker server for functional tests to access.
// The script waits for the server to start and prints some logs
// to help with debugging.

const fs = require('fs');
const path = require('path');
const childProcess = require('child_process');

const root = path.join(__dirname, '..');
console.log(`Working directory: ${root}`);
const containerIdFile = path.join(root, 'docker-container-id.txt');

// TODO: make these configurable when we need to start
// servers for multiple apps.
const appInstance = 'disco';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using process.env.NODE_APP_INSTANCE would be a pretty easy fix here I think and buy us most of that. I think it's already passed to the tests on travis... if not you could easily add it to the env vars.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I thought so too but passing env vars to commands in .travis.yml is not straight forward. I think it's best to deal with it later. I think we may have to create an amo and a disco package.json script.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, no problem.

const nodeEnv = 'uitests';

function logDivider(heading) {
console.log(`${'='.repeat(35)} ${heading} ${'='.repeat(35)}`);
}

function shell(cmd, args) {
// Execs a command as if it were part of the parent shell
// (i.e. the output is unbuffered and is displayed in the console).
logDivider('BEGIN shell');
const cmdString = `${cmd} ${args.join(' ')}`;
console.log(`Shell: ${cmdString}`);
return new Promise((resolve, reject) => {
const child = childProcess.spawn(cmd, args, {
cwd: root,
});

child.stdout.on('data', (data) => {
process.stdout.write(data.toString());
});
child.stdout.on('error', (error) => reject(error));

child.stderr.on('data', (data) => {
process.stderr.write(data.toString());
});
child.stderr.on('error', (error) => reject(error));

child.on('error', (error) => reject(error));
child.on('exit', (exitCode) => {
logDivider('END shell');
if (exitCode !== 0) {
reject(new Error(
`shell command failed: ${cmdString} (exit: ${exitCode || '[empty]'})`));
}
resolve();
});
});
}

function exec(cmd, argParts, { quiet = false } = {}) {
const cmdString = `${cmd} ${argParts.join(' ')}`;
if (!quiet) {
logDivider('BEGIN exec');
console.log(`Exec: ${cmdString}`);
}
return new Promise((resolve, reject) => {
childProcess.exec(cmdString, { cwd: root }, (error, stdout, stderr) => {
let hasOutput = false;
if (stdout) {
hasOutput = true;
if (!quiet) {
logDivider('BEGIN stdout');
process.stdout.write(stdout.toString());
logDivider('END stdout');
}
}
if (stderr) {
hasOutput = true;
if (!quiet) {
logDivider('BEGIN stderr');
process.stderr.write(stderr.toString());
logDivider('END stderr');
}
}
if (error) {
if (!hasOutput) {
console.warn('The command did not return any output');
}
if (!quiet) {
// Don't log all of the error because it includes all of stderr.
console.error(`Snippet of exec error: ${error.toString().slice(0, 60)}...`);
}
reject(new Error(`exec command failed: ${cmdString}`));
}
resolve(stdout.toString());
});
});
}

function fileExistsSync(file) {
try {
return Boolean(fs.statSync(file));
} catch (error) {
return false;
}
}

new Promise((resolve) => {
if (fileExistsSync(containerIdFile)) {
console.warn(`Removing existing container ID file: ${containerIdFile}`);
fs.unlinkSync(containerIdFile);
}
resolve();
})
.then(() => exec('docker', ['build', '-q', '.']))
.then((imageIdOutput) => imageIdOutput.trim())
.then((imageId) => {
// Start the server.
const runArgs = [
'run',
'-d',
'-p=4000:4000',
'-e',
`NODE_APP_INSTANCE=${appInstance}`,
'-e',
`NODE_ENV=${nodeEnv}`,
`--cidfile=${containerIdFile}`,
// This will make sure we can read the logs.
'--log-driver=json-file',
imageId,
'/bin/sh -c "npm run build && npm run start"',
];
return exec('docker', runArgs);
})
.then(() => {
return fs.readFileSync(containerIdFile).toString();
})
.then((containerId) => {
// Wait for the server to start and build assets.

// This is the subresource integrity file, one of several asset files
// built when the server starts.
const sampleAssetFile = '/srv/code/dist/sri.json';

return new Promise((resolve, reject) => {
// All time is in milleseconds.
const interval = 1000;
const timeOut = 1000 * 60 * 5; // 5 minutes
let timeElapsed = 0;
console.log(`Waiting for assets to build (looking for ${sampleAssetFile})`);

const waitForAssets = () => {
if (timeElapsed >= timeOut) {
reject(new Error(
`Timed out waiting for assets file to appear at
${sampleAssetFile}`));
return;
}
timeElapsed += interval;

exec('docker',
['exec', containerId, 'ls', sampleAssetFile], { quiet: true }
)
.then(() => {
// The file exists, the server has finished building assets.
resolve(containerId);
})
.catch(() => {
// The file does not exist yet. Try again.
setTimeout(waitForAssets, interval);
});
};

waitForAssets();
});
})
.then((containerId) => {
// Since the asset we just checked for isn't the final asset built,
// wait just a bit before capturing the logs.
return new Promise(
(resolve) => setTimeout(() => resolve(containerId), 2000));
})
.then((containerId) => {
// Show all of the server logs.
return shell('docker', ['logs', '--tail=all', containerId]);
})
.then(() => {
console.log('The server is ready 🦄 ✨');
})
.catch((error) => {
console.log(''); // Pad with a blank line
console.error(error.stack);
process.exit(1);
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"stylelint": "stylelint --syntax scss **/*.scss",
"lint": "npm run eslint && npm run stylelint",
"start": "npm run version-check && NODE_PATH='./:./src' node bin/server.js",
"start-func-test-server": "node bin/start-func-test-server.js",
"test-ci": "bin/config-check.js && better-npm-run test-ci && cat ./coverage/lcov.info | coveralls",
"test": "bin/config-check.js && jest --watch",
"test-coverage": "bin/config-check.js && jest --coverage --watch",
Expand Down
9 changes: 6 additions & 3 deletions tests/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,23 @@ The tests must be run in Firefox 48 or later.
1. Install [Tox]
1. Download geckodriver [v0.14.0][geckodriver] and ensure it's executable and
in your path
1. Run `tox`
1. Make sure you have [docker][] installed and start the server with
`yarn start-func-test-server`
1. Run `PYTEST_BASE_URL=http://localhost:4000 tox`

To run against a different environment, set the `PYTEST_BASE_URL` environment
To run against a different environment, change the `PYTEST_BASE_URL` environment
variable, like so:

```bash
PYTEST_BASE_URL=https://discovery.addons.mozilla.org tox
PYTEST_BASE_URL=https://discovery.addons.allizom.org tox
```

The pytest plugin that we use for running tests has a number of advanced
command line options available. To see the options available, run
`pytest --help`. The full documentation for the plugin can be found
[here][pytest-selenium].

[docker]: https://www.docker.com/
[git-clone]: https://help.github.com/articles/cloning-a-repository/
[git-fork]: https://help.github.com/articles/fork-a-repo/
[geckodriver]: https://github.com/mozilla/geckodriver/releases/tag/v0.14.0
Expand Down