updating docs

This commit is contained in:
Mason Houtz
2021-02-22 09:25:04 -08:00
parent a715ff5c2a
commit b9533e8874
13 changed files with 290 additions and 251 deletions
-135
View File
@@ -164,138 +164,3 @@ terminal this starts for executing Jest tests.
yarn run jest-watch Dialog
yarn run jest-watch workflow/run
### Writing a test file
Jest will try to test any file ending in "\*.test.js". Please place your test
files inside client/src folders right next to whatever files that they are
testing.
Jest has extensive documentation on the expect API, mocking, and more on the
[official docs page](https://jestjs.io/docs/en/getting-started.html), which will
be your best resource here.
```javascript
// yourtestfile.test.js
import { things } from "./yourtestfile.js";
describe("some module you wrote", () => {
let transientVariables;
let serviceInstances;
let testData;
beforeEach(() => {
// setup your code (if necessary)
});
afterEach(() => {
// teardown your code (so it doesn't ruin the next test)
});
it("should do something or other", () => {
expect(workflowNodeCount()).toBe(5);
});
});
```
### Testing ~~Suggestions~~ Obligations
#### Clearly document the intent of your test
Please remember that these tests are not _for_ you. They're for the people who
come after you. It will be a lot easier to modify, repair and upgrade your code
if they can figure out what you were originally hoping to accomplish. Try to
use as detailed 'expect' statements as possible -- overuse of 'toBeTruthy()'
for example, can hide the intent of your test.
Add a couple of comments. Use variable names that mean something. Nobody's
code is as self-documenting as they believe it to be.
#### Only test the public API that you define (carefully!)
Internal implementations come and go with library upgrades and new tech. But
the point of the unit test is to make sure your units work as designed....
which means you need to... you know... design your code to work in units.
Separate your concerns and identify the developer-facing methods and functions
you expect them to use. Test THOSE. Everything else should probably be
considered an implementation detail.
The other side of the same coin is to test *only* the unit in question. If your
component has a model that uses a service that touches Vuex, which then uses
Axios to fetch some data -- don't test all that at once. Break things apart and
mock functionality to isolate testing to units. End to end testing is a
separate thing that shouldn't be attempted using spec tests in Jest.
Assume nobody cares _how_ your code works, we just need to know that the public
API you designed _does_ work. If performance problems or new tech necessitate a
re-write, these tests become a guide for the next implementation.
#### Wrap native browser resources in a function so they can be easily mocked
If your javascript needs to talk to the window object, or navigator, etc. wrap
that in a function call so that it can be easily mocked during testing.
```javascript
// myModule.js
// ... other code
export function redirectTo(url) {
window.location = url;
}
// myModule.test.js
jest.mock("myModule", () => ({
redirectTo: (url) => {
console.log(`I would have gone to: ${url}`);
},
}));
describe("some module", () => {
// ....
it("Performs a redirect when Foo is clicked"){
expect(redirectTo.mock.calls.length).toBe(1);
}
});
```
#### Implement logic in pure functions when possible
The more of your logic that is written in deterministic functions (i.e. no
side-effects, same inputs always result in same outputs) the easier it is to
test. Just load up the functions and supply suitable test inputs.
There is almost definitely no such thing as a well-written 1000 line function.
Most of whatever happened in that thing was probably deterministic and can be
broken up into easily testable chunks.
### Specific test scenarios & examples
[Mocking an imported
dependency](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
[Testing async
operations](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
[Testing a Vue component for expected rendering
output](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
[Firing an event against a shallow mounted vue
component](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
### The dirty secret about testing
It's good to have tests, but testing code isn't really about testing at all,
it's actually about software design.
Maintainable software is testable software. If you can run a unit test on your
code, then that means you must have necessarily separated your code into
testable units and you will have almost definitely written better, more
modular, more easily manipulated code, and nobody will ever contemplate using
git-blame to figure out what went wrong.
Probably.
@@ -1,14 +0,0 @@
Don't directly reference window.Galaxy in Vue components
You've got components. Components take props. Please pass in any values you might need from
window.Galaxy as props and avoid referencing global Galaxy inside your components. I've even created
basic providers which give you access to the Galaxy.config, current user, and current user
histories. Please use them to retrieve your values, and bypass importing Galaxy altogether.
There are use-cases where the Backbone models update over time and we need to update some value
inside Vue. Let me help you solve those problems instead of importing backbone models into Vue
components. Usually the answer is a backbone event listener that updates some relevant Vuex store.
When writing a new component, your goal should be to replace old Galaxy functionality, not to
repackage it in another format so we continue to have Galaxy's inherent problems, the most important
of which is...
@@ -0,0 +1,77 @@
Without a doubt. The most problematic part of the old client is the way every important piece of
information was hung on a global variable whose initialization is completely unregulated.
### Don't directly reference window.Galaxy in Vue components
Components take props. Please pass in any values you might need from window.Galaxy as props and
avoid referencing global Galaxy inside your components. I've even created basic providers which give
you access to the Galaxy.config, current user, and current user histories. Please use them to
retrieve your values, and bypass importing Galaxy altogether.
#### Sometimes you still need to update Vue from backbone as the legacy environment changes
There are definitely use-cases where the Backbone models update over time and we need to update some
value inside Vue. Let me help you solve those problems instead of importing backbone models into Vue
components. Usually the answer is a backbone event listener that updates some relevant Vuex store.
* [Keeping Vuex in Sync with
Galaxy](https://github.com/galaxyproject/galaxy/blob/dev/client/src/store/syncVuexToGalaxy.js)
These issues should disappear over time as the all of the old client is rebuilt in the new
ecosystem.
## Mount Functions
When writing a new component, your goal should be to replace old Galaxy functionality, not to
repackage it in another format thereby persisting Galaxy's inherent problems.
In what most people think of as a "standard" Vue application there would be only one place that Vue
is mounted to the HTML environment, and that would be in a main.j or an app.js. Most modern
single-page applications only have one starting point like that.
However, we are incrementally replacing old Backbone views, so in its current state, Galaxy may have
several mounting functions for various components depending on where that component is intended to
fit into the existing Backbone layouts.
### Use the standard mount function
A standard mount function has been provided in src/utils. This mount function accepts a component
definition, and then allows you to start up your Vue component with our standard load-out of plugins
including localization, Vuex, and a couple other utilities. This is the preferred way to mount your
component inside the old Backbone layout until the application is fully converted.
```js static
// mountMyComponent.js
import { mountVueComponent } from "utils/mountVueComponent";
import MyComponent from "./MyComponent";
// this creates a function that will accept a props object and a DOM element on which to mount
const mounter = mountVueComponent(MyComponent);
export default mounter;
```
### Using the standard mount to pass in Galaxy variables as props
```js static
import { getGalaxyInstance } from "app";
import mountMyComponent from "./mountMyComponent";
const HorribleBackboneView = {
someInitMethodYouMake() {
const Galaxy = getGalaxyInstance();
// pass in required props
const props = {
somePropVal: Galaxy.someDealie,
name: Galaxy.currentHistory.name,
};
// VM is a Vue instance.
// this.$el is some jquery selection, first item is the actual DOM object
const vm = mountMyComponent(props, this.$el[0]);
}
}
```
@@ -1,14 +0,0 @@
### Classes exist but they aren't as important as you may be used to.
You may come from a class-based programming background and think that your first step should be to
make a class hierchy that does the thing you want. It doesn't help that Vue superficially looks like
a class definition, (even though what you're really doing is configuring an Observable tree).
### Javascript is a pretty functional language.
Javascript is largely a functional language. Its class support is limited and less useful, and
unless we switch to Typescript, we don't even have interfaces or typing, arguably the most useful
parts of a class-based language.
In general, you will get more mileage out of javascript by embracing functional programming
approaches because that is what Javascript is good at.
@@ -11,4 +11,8 @@ site whether you want it or not. jQuery leverages an outdated initialization par
hugely problematic when it comes to unit testing and module building.
One of our most important goals in redesigning Galaxy is the complete elimination of this library
from our source, along with all its invasive plugins.
from our source, along with all its invasive plugins.
### References
* [You Don't Need jQuery](https://github.com/nefe/You-Dont-Need-jQuery)
* [document.querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
@@ -3,7 +3,7 @@ probably look familiar to anybody whis is already passingly familiar with Vue. H
information comes in as properties, any internal variables get defined in "data", changes go out as
events.
## Composition Example
### Composition Example
```html static
<!-- Use of a renderless component with a display component -->
@@ -0,0 +1,71 @@
### A component is really just a fancy function
I'm not talking about how webpack turns it into a rendering function. That's obvious.
I mean conceptually, props come in (like arguments) and events go out (like the return statements).
A component is a fancy kind of function that can keep emitting results and accept changing inputs
over time. In truth it more closely resembles an Observable, but an observable is ALSO a slightly
fancier kind of function.
If you just think of a component as thing that takes input props and emits output events you're well
on your way to using them well.
The worst components are ones that might as well just be a big single page script. That's zero
percent better than the spaghetti we're working so hard to replace. That's just repackaging all the
problems of the old imperative class-based legacy code.
### Get comfortable with events, limit your dependence on Vuex
New vue programmers are ok at handing props to components, but they rarely use events effectively
(at first). As a result they end up using a lot of global state, a million little data props and
relying on imperfect globalized tools like Vuex or other imported dependencies for every little
variable.
Vuex definitely has its uses, but not as many as you might expect given the way it is
overly-emphasized in common tutorials. It's easy to walk away from an "Intro to Vue" video with the
idea that all data must live in Vuex all the time. That's a really undesirable situation.
Although vuex is a well-organized (many would say over-organized) state machine, it is important
to remember that it is still a kind of global injection and deserves to be considered as such.
* [Should I Store This Data in
Vuex](https://markus.oberlehner.net/blog/should-i-store-this-data-in-vuex/)
* [Vuex getters are great, but don’t overuse
them](https://codeburst.io/vuex-getters-are-great-but-dont-overuse-them-9c946689b414)
Data persistence should be something that happens near the top of your component tree, not down in
the guts.
Your first thought with a component should be: "How can I offload the handling of the results of
this component to my caller?" The answer is usually going to be events. A component that simply
accepts props and emits events can be re-used in more contexts than one that relies on an external
global state to operate.
#### Read up on .sync and v-model
They're just fancy shorthands for a prop / event handler combination. They are fundamentally no
different from props and events, but the syntax is important to understand.
### Think carefully about what should really be in "data".
Most of good component design boils down to answering the following question: What do I want to put
in data, computed, and props?
Data is the place where temp data goes that is not sensible to persist in Vuex or other
application-wide global state, usually because its use is very specific to the operation of this
particular component. There really should only be a few variables in data.
#### Break down your own internal dependencies
Most components only need one or two variables in data. If you have a large amount of data
variables, it's worth taking a little time to stop and build a mini-dependency tree for yourself.
You will probably find that almost everything can be written in terms of computed transformations on
a small number of data and properties.
Defining your values using computes will lead to a lot less "oh I forgot to update that
variable"-style bugs.
If you have more than a few data variables you probably (a) aren't leveraging computeds and
properties, or (b) are trying to implement too many features for just one component. It is important
separate your concerns in components just like you do in any other kind of programming.
@@ -1,3 +0,0 @@
## Wrap access to browser-native resources in functions
Unit testing in a modern javascript application runs in node without a browser.
@@ -0,0 +1,20 @@
[Galaxy uses Jest](https://jestjs.io/) for its client-side unit testing
framework.
For testing Vue components, we use the [Vue testing
utils](https://vue-test-utils.vuejs.org/) to mount individual components in a
test bed and check them for rendered features. Please use jest-based mocking
for isolating test functionality.
### Specific test scenarios & examples
* [Mocking an imported
dependency](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
* [Testing async
operations](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
* [Testing a Vue component for expected rendering
output](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
* [Firing an event against a shallow mounted vue
component](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
@@ -0,0 +1,42 @@
Part of making good code is making that code easy to test.
### Implement logic in pure functions when possible
The more of your logic that is written in deterministic functions (i.e. no
side-effects, same inputs always result in same outputs) the easier it is to
test. Just load up the functions and supply suitable test inputs.
There is almost definitely no such thing as a well-written 1000 line function.
Most of whatever happened in that thing was probably deterministic and can be
broken up into easily testable chunks.
### Wrap native browser resources in a function so they can be easily mocked
If your javascript needs to talk to the window object, or navigator, etc. wrap
that in a function call so that it can be easily mocked during testing.
```js static
// myModule.js
// ... other code
export function redirectTo(url) {
window.location = url;
}
// myModule.test.js
jest.mock("myModule", () => ({
redirectTo: (url) => {
console.log(`I would have gone to: ${url}`);
},
}));
describe("some module", () => {
// ....
it("Performs a redirect when Foo is clicked"){
expect(redirectTo.mock.calls.length).toBe(1);
}
});
```
@@ -0,0 +1,73 @@
### Clearly document the intent of your test
Please remember that these tests are not _for_ you. They're for the people who
come after you. It will be a lot easier to modify, repair and upgrade your code
if they can figure out what you were originally hoping to accomplish. Try to
use as detailed 'expect' statements as possible -- overuse of 'toBeTruthy()'
for example, can hide the intent of your test.
Add a couple of comments. Use variable names that mean something. Nobody's
code is as self-documenting as they believe it to be.
### Only test the public API that you define
Internal implementations come and go with library upgrades and new tech. But
the point of the unit test is to make sure your units work as designed....
which means you need to... you know... design your code to work in units.
Separate your concerns and identify the developer-facing methods and functions
you expect them to use. Test THOSE. Everything else should probably be
considered an implementation detail.
The other side of the same coin is to test *only* the unit in question. If your
component has a model that uses a service that touches Vuex, which then uses
Axios to fetch some data -- don't test all that at once. Break things apart and
mock functionality to isolate testing to units. End to end testing is a
separate thing that shouldn't be attempted using spec tests in Jest.
Assume nobody cares _how_ your code works, we just need to know that the public
API you designed _does_ work. If performance problems or new tech necessitate a
re-write, these tests become a guide for the next implementation.
### Writing a test file
Jest will try to test any file ending in "\*.test.js". Please place your test
files inside client/src folders right next to whatever files that they are
testing.
Jest has extensive documentation on the expect API, mocking, and more on the
[official docs page](https://jestjs.io/docs/en/getting-started.html), which will
be your best resource here.
```js static
// yourcode.test.js
import { things } from "./yourcode.js";
describe("some module you wrote", () => {
let transientVariables;
let serviceInstances;
let testData;
beforeEach(() => {
// setup your code (if necessary)
});
afterEach(() => {
// teardown your code (so it doesn't ruin the next test)
});
it("should do something or other", () => {
expect(workflowNodeCount()).toBe(5);
});
});
```
### Check out the Jest helper functions
We have created some [common helpers for common testing
scenarios](https://github.com/galaxyproject/galaxy/blob/dev/client/tests/jest/helpers.js).
@@ -1,82 +0,0 @@
## Understand that a component is really just a fancy function
I'm not talking about how webpack turns it into a rendering function. That's obvious.
I mean conceptually, props come in (like arguments) and events go out (like the return statements).
A component is a fancy kind of function that can keep emitting results and accept changing inputs
over time. In truth it more closely resembles an Observable, but an observable is ALSO a slightly
fancier kind of function.
If you just think of a component as thing that takes inputs and emits outputs you're well on your
way to using them properly.
The worst components are ones that might as well just be a big single script that runs. That's zero
percent better than the spaghetti we're working so hard to replace. That's just repackaging all the
problems of the old imperative class-based legacy code.
## Javascript is a pretty functional language. Classes exist but they aren't as important as you may be used to.
You may come from a class-based programming background and think that your first step should be to
make a class hierchy that does the thing you want. It doesn't help that Vue superficially looks like
a class definition, (even though what you're really doing is configuring an Observable tree).
But javascript is largely a functional language. Its class support is limited and less useful, and
unless we switch to Typescript, we don't even have interfaces or typing, arguably the most useful
parts of a class-based language.
In general, you will get more mileage out of javascript by embracing functional programming
approaches because that is what Javascript is good at.
## Get comfortable with events, limit your dependence on Vuex
New vue programmers are ok at handing props to components, but they rarely use events effectively
(at first). As a result they end up using a lot of global state, a million little data props and
relying on imperfect globalized tools like Vuex or other imported dependencies for every little
variable.
Vuex has its uses, but not as many as you might think. Data persistence should be something that
happens near the top of your component tree, not down in the guts.
Your first thought with a component should be: "How can I offload the handling of the results of
this component to my caller?" The answer is usually going to be events. A component that simply
accepts props and emits events can be re-used in more contexts than one that relies on an external
global state to operate.
### Read up on .sync and v-model
They're just fancy shorthands for a prop / event handler combination. They are fundamentally no
different from props and events, but the syntax is important to understand.
## Think carefully about what should really be in "data".
Most of good component design boils down to answering the following question: What do I want to put
in data, computed, and props?
Data is the place where temp data goes that is not sensible to persist in Vuex or other
application-wide global state, usually because its use is very specific to the operation of this
particular component. There really should only be a few variables in data.
### Break down your own internal dependencies
Most components only need one or two variables in data. If you take the time to analyze your own
internal dependency tree, you will probably find that almost everything can be written in terms of
computed transformations on a small number of data and propertie
If you have more than a few variables you (a) aren't leveraging computeds and properties, or (b) are
trying to implement too many features for just one component. It is important separate your concerns
in components just like you do in any other kind of programming.
## Have a plan
Create a design plan and an abstraction for the way the guts of your component work. Think about the
way data passes from parent components to children and back again. Break your component into
sub-components just like you would break a class into sub-methods, the same exact principles apply.
Don't just dump a pile of spaghetti into a component, that is no better than the legacy code we are
replacing.
+1 -1
View File
@@ -34,7 +34,7 @@ function getSections() {
const cmpPath = path.join(__dirname, "src/components");
const { rootNode: componentDocs } = getDocSections(cmpPath, { ignore: problemChildren });
return [design, componentDocs, styles];
return [design, styles, componentDocs];
}
module.exports = {