Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation #19

Merged
merged 8 commits into from
Apr 22, 2019
Merged
Show file tree
Hide file tree
Changes from 5 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
@@ -0,0 +1 @@
/packages/deploy*
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Milestones [![Build Status](https://travis-ci.org/salsify/milestones.svg?branch=master)](https://travis-ci.org/salsify/milestones)

The `@milestones` packages provide a set of tools for navigating concurrent code in testing and development. Milestones act as named synchronization points, and they give you the ability to change the behavior of annotated code during testing, skipping pauses or mocking out results.

Full interactive documentation can be found at https://salsify.github.io/milestones.

## Packages

This library is broken out into several packages:
- [@milestones/core](packages/core): the core library, containing tools for defining and interacting with milestones
- [@milestones/babel-plugin-strip-milestones](packages/babel-plugin-strip-milestones): a Babel plugin that removes milestone definitions from your code, ensuring no overhead in production
- [@milestones/ember](packages/ember): an Ember addon that integrates with the framework runtime and build system to provide zero-config setup for working with milestones
9 changes: 9 additions & 0 deletions packages/babel-plugin-strip-milestones/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# @milestones/babel-plugin-strip-milestones

This package allows you to strip the `milestone()` calls completely out of your code to eliminate any overhead in production.

Full documentation can be found at https://salsify.github.io/milestones/docs/babel-plugin.

## Usage

Include `@milestones/babel-plugin-strip-milestones` in your package dependencies and then add `'@milestones/strip-milestones'` to the `plugins` array in your `babel.config.json` or the corresponding configuration location for your particular Babel integration.
dfreeman marked this conversation as resolved.
Show resolved Hide resolved
55 changes: 55 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# @milestones/core

Milestones are a tool for annotating points in time in your code.

A milestone can act as a named synchronization point, allowing you to ensure the state of your application code at the moment in time in your testing when you wish to make assertions. They also give you the ability to change the behavior of annotated code during testing, skipping pauses or mocking out results.

Full interactive documentation can be found at https://salsify.github.io/milestones.

## Example

#### Application Code
```ts
import { milestone } from '@milestones/core';

export const Save = Symbol('save');
export const ShowCompletion = Symbol('show-completion-message');

// ...

async function save() {
renderMessage('Saving...');
await milestone(Save, () => saveData());

renderMessage('Saved!');
await milestone(ShowCompletion, () => sleep(4000));

renderMessage('');
}
```

#### Test Code
```ts
import { advanceTo } from '@milestones/core';
import { Save, ShowCompletion } from '../app/code/under/test';

// ...

it('renders the current saving status', async () => {
click('.save-button');

// Wait until we start saving, then check that
// the expected message is being shown.
let saveHandle = await advanceTo(Save);
expect(currentMessage()).to.equal('Saving...');

// Now go ahead and perform the save
await saveHandle.continue();
expect(currentMessage()).to.equal('Saved!');

// Now advance to where we pause to show the
// the status message, but skip the sleep
await advanceTo(ShowCompletion).andReturn();
expect(currentMessage()).to.equal('');
});
```
5 changes: 2 additions & 3 deletions packages/ember/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
},
rules: {
'ember/avoid-leaking-state-in-ember-objects': 'off',
'@typescript-eslint/no-var-requires': 'off',
},
overrides: [
// node files
Expand All @@ -36,9 +37,7 @@ module.exports = {
node: true,
},
plugins: ['node'],
rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
'@typescript-eslint/no-var-requires': 'off',
}),
rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {}),
},
],
};
25 changes: 25 additions & 0 deletions packages/ember/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# @milestones/ember

The `@milestones/ember` addon integrates with the Ember runtime and ember-cli to provide zero-config setup for working with milestones.
Copy link

Choose a reason for hiding this comment

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

Could you say something about what milestones addresses that is not covered by the Ember built-ins explained here?
https://guides.emberjs.com/release/testing/acceptance/

Maybe the reader can deduce from the Core docs, but I think it would help to be more explicit.
The official Ember docs imply, perhaps unintentionally, that the ember test helpers already provide all you need to test asynchronous code.

In a few Salsify projects we've used milestones heavily, but since it's not (yet) a widely used package, I've wondered how other teams tackle these problems. A mocking library like sinon? Simply not writing certain kinds of tests?

A couple sentences, or maybe a before/after example as in the ember-concurrency docs, could clarify when it's the right tool.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a great point. I had a bit written up about this (and it was also the bulk of the content in the mini-talk I gave at the Boston meetup last fall), but it got lost in the shuffle as I was writing everything else up.

I'll re-integrate that into the Ember page on the site (my goal is to keep these READMEs as lightweight as possible and push people toward the richer interactive site), and I'll also try and add a bit of more general-purpose motivation to the core docs as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've wondered how other teams tackle these problems. A mocking library like sinon? Simply not writing certain kinds of tests?

Broadly speaking I think people tend to go one of three ways, depending on their personal priorities:

  • value coverage and reliability: sprinkle sleeps throughout code to always ensure you've settled in the right state, but accept that your test suite will be slow
  • value coverage and speed: make a best effort at ensuring state, but live with the fact that tests that pass in one environment may well fail in another
  • value speed and reliability: unit test what you can, and accept that there are some things you just don't have integration coverage for

I've done all of those at different points in the past, at Salsify and elsewhere, in codebases with and without Ember. My general experience is that projects tend to be in a constant state of flux between those different tradeoffs, depending on the values of the person who most recently 'fixed' the test suite.


Full documentation can be found at https://salsify.github.io/milestones/docs/ember.

## Runtime

All milestones are configured by `@milestones/ember` to use RSVP for their promise implementation, ensuring that any code that executes as a result of a milestone occurs within a runloop.

## Build

By default, milestones will be stripped from your code in production builds using `@milestones/babel-plugin-strip-milestones`. This behavior is always controlled by the host application, and it can be overridden in the host's `ember-cli-build.js`.

```ts
let app = new EmberApp(defaults, {
milestones: {
stripMilestones: false, // or whatever your preference
},
});
```

## Ember Concurrency

If `ember-concurrency` is present in your project, any milestones you create will be task-like promises that will bubble cancelation appropriately. They will also be cancelable from your test code.
10 changes: 10 additions & 0 deletions packages/ember/config/addon-docs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-env node */
'use strict';

// eslint-disable-next-line node/no-unpublished-require
const AddonDocsConfig = require('ember-cli-addon-docs/lib/config');

module.exports = class extends AddonDocsConfig {
// See https://ember-learn.github.io/ember-cli-addon-docs/docs/deploying
// for details on configuration you can override here.
};
29 changes: 29 additions & 0 deletions packages/ember/config/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-env node */
'use strict';

module.exports = function(deployTarget) {
let ENV = {
build: {},
// include other plugin configuration that applies to all deploy targets here
};

if (deployTarget === 'development') {
ENV.build.environment = 'development';
// configure other plugins for development deploy target here
}

if (deployTarget === 'staging') {
ENV.build.environment = 'production';
// configure other plugins for staging deploy target here
}

if (deployTarget === 'production') {
ENV.build.environment = 'production';
// configure other plugins for production deploy target here
}

// Note: if you need to build some configuration asynchronously, you can return
// a promise that resolves with the ENV object instead of returning the
// ENV object synchronously.
return ENV;
};
23 changes: 23 additions & 0 deletions packages/ember/ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,32 @@ module.exports = function(defaults) {
stripMilestones: `${process.env.STRIP_MILESTONES}` === 'true',
},

autoImport: {
webpack: {
node: {
fs: 'empty',
},
},
},

cssModules: {
plugins: {
before: [require('postcss-nested')], // eslint-disable-line node/no-unpublished-require
},
},

ace: {
themes: ['tomorrow_night_bright'],
modes: ['javascript'],
},

'ember-cli-babel': {
throwUnlessParallelizable: true,
},

'ember-cli-addon-docs-typedoc': {
packages: ['@milestones/core', '@milestones/ember', '@milestones/babel-plugin-strip-milestones'],
},
});

/*
Expand Down
24 changes: 23 additions & 1 deletion packages/ember/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,50 @@
"ember-cli-typescript": "^2.0.0"
},
"devDependencies": {
"@babel/core": "^7.4.3",
"@dfreeman/ember-cli-addon-docs-typedoc": "^0.1.0",
"@ember/optional-features": "^0.6.3",
"@ember/render-modifiers": "^1.0.0",
"@fortawesome/ember-fontawesome": "^0.1.13",
"@fortawesome/free-solid-svg-icons": "^5.8.1",
"@types/ace": "^0.0.42",
"@types/common-tags": "^1.8.0",
"@types/ember": "^3.1.0",
"@types/ember-qunit": "^3.4.6",
"@types/ember-resolver": "^5.0.6",
"@types/ember__test-helpers": "^0.7.8",
"@types/qunit": "^2.5.4",
"chai": "^4.2.0",
"common-tags": "^1.8.0",
"ember-ace": "^2.0.1",
"ember-basic-dropdown": "^1.1.2",
"ember-cli": "~3.8.1",
"ember-cli-addon-docs": "^0.6.9",
"ember-cli-dependency-checker": "^3.1.0",
"ember-cli-deploy": "^1.0.2",
"ember-cli-deploy-build": "^1.1.1",
"ember-cli-deploy-git": "^1.3.3",
"ember-cli-deploy-git-ci": "^1.0.1",
"ember-cli-htmlbars": "^3.0.0",
"ember-cli-htmlbars-inline-precompile": "^1.0.3",
"ember-cli-inject-live-reload": "^1.8.2",
"ember-cli-typescript-blueprints": "^2.0.0",
"ember-cli-uglify": "^2.1.0",
"ember-concurrency": "^0.9.0",
"ember-css-modules": "^1.1.2",
"ember-decorators-polyfill": "^1.0.3",
"ember-load-initializers": "^1.1.0",
"ember-maybe-import-regenerator": "^0.1.6",
"ember-qunit": "^3.4.1",
"ember-resolver": "^5.0.1",
"ember-source": "~3.8.0",
"ember-truth-helpers": "^2.1.0",
"eslint": "^5.15.3",
"eslint-plugin-ember": "^6.3.0",
"eslint-plugin-node": "^8.0.1",
"loader.js": "^4.7.0",
"postcss-nested": "^4.1.2",
"pretender": "^3.0.1",
"qunit-dom": "^0.8.0",
"typescript": "^3.3.4000"
},
Expand All @@ -59,5 +80,6 @@
},
"ember-addon": {
"configPath": "tests/dummy/config"
}
},
"homepage": "https://salsify.github.io/milestones"
}
2 changes: 1 addition & 1 deletion packages/ember/tests/acceptance/loop-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { advanceTo, setupMilestones } from '@milestones/core';
import { setupApplicationTest } from 'ember-qunit';
import { module, test } from 'qunit';
import Env from 'dummy/config/environment';
import { TickTimer } from 'dummy/routes/loop';
import { TickTimer } from 'dummy/pods/loop/route';

if (!Env.STRIP_MILESTONES) {
module('Acceptance | infinite loops', function(hooks) {
Expand Down
90 changes: 90 additions & 0 deletions packages/ember/tests/acceptance/playgrounds-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { visit } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { module, test } from 'qunit';
import Env from 'dummy/config/environment';
import Pretender from 'pretender';
import { deactivateAllMilestones, activateMilestones } from '@milestones/core';
import { StepComplete } from 'dummy/utils/evaluation';

interface DocsRoutesService {
routeUrls: string[];
}

if (!Env.STRIP_MILESTONES) {
const MAX_STEPS = 50;
const URLS = [
'/',
'/docs',
'/docs/interacting-with-milestones',
'/docs/milestone-keys',
'/docs/ember',
'/docs/the-playground',
];

module('Acceptance | playgrounds', function(hooks) {
setupApplicationTest(hooks);

hooks.afterEach(() => deactivateAllMilestones());

let pretender: Pretender;
hooks.beforeEach(function() {
pretender = new Pretender(function() {
this.get('https://httpbin.org/get', req => {
let args = (req as { params: unknown }).params;
return [200, {}, JSON.stringify({ args })];
});

// @ts-ignore
this.get('/docs/*', this.passthrough);
this.get('/versions.json', () => [404, {}, '']);
});
});

hooks.afterEach(function() {
pretender.shutdown();
});

test('every docs page with playgrounds is tested', async function(assert) {
await visit('/docs');

let docsRoutes = this.owner.lookup('service:docs-routes') as DocsRoutesService;
for (let url of docsRoutes.routeUrls) {
if (URLS.includes(url)) continue;

await visit(url);

assert.notOk(this.element.querySelector('[data-test="playground"]'), `${url} has no playgrounds on it`);
}
});

for (let url of URLS) {
test(`every playground on ${url} runs to completion`, async function(assert) {
await visit(url);

playgrounds: for (let [i, playground] of this.element.querySelectorAll('[data-test="playground"]').entries()) {
let stepButton = playground.querySelector('[data-test="step"]')! as HTMLButtonElement;
let resetButton = playground.querySelector('[data-test="reset"]')! as HTMLButtonElement;
let output = playground.querySelector('[data-test="output"]')! as HTMLDivElement;

for (let step = 0; step < MAX_STEPS; step++) {
stepButton.click();
let coordinator = activateMilestones([StepComplete]);
await coordinator.advanceTo(StepComplete);
coordinator.deactivateAll();

if (step === 0) {
assert.notOk(resetButton.disabled, `Playground ${i} on doesn't immediately error`);
}

if (resetButton.disabled) {
assert.notOk(output.innerText.includes('Uncaught error'), `Playground ${i} on completes without error`);
continue playgrounds;
}
}

assert.ok(false, `Playground ${i} completes in under ${MAX_STEPS} steps`);
}
});
}
});
}
3 changes: 3 additions & 0 deletions packages/ember/tests/dummy/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import Resolver from './resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';

// Ensure the plugin code is included in API docs
import '@milestones/babel-plugin-strip-milestones';

const App = Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Expand Down
17 changes: 17 additions & 0 deletions packages/ember/tests/dummy/app/helpers/milestones-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { helper } from '@ember/component/helper';
import { htmlSafe } from '@ember/string';
import styles from './styles';

// @ts-ignore
import milestonesSVG from 'ember-svg-jar/inlined/milestones-icon';

export default helper((_params: unknown[], hash: Record<string, unknown>) => {
return htmlSafe(`
<div class="${hash.class || ''}">
<svg viewBox="0 25 256 240" class="${styles.svg}">
${milestonesSVG.content}
</svg>
Milestones
</div>
`);
});
9 changes: 9 additions & 0 deletions packages/ember/tests/dummy/app/helpers/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.svg {
width: 1em;
vertical-align: middle;
margin-right: 0.15em;

path {
fill: var(--brand-primary);
}
}
Loading