This Ember addon provides a few opnionated techniques to make working with Cucumber easier:
- A compact library of fully reusable steps.
- An easy way to compose steps from multiple sources.
It is a companion for the ember-cli-yadda addon which in turn leverages the Yadda library -- an implementation of Cucumber.
- Status & compatibility
- Rationale
- Installation
- Usage
- Project structure
- Composable step files
- Composing steps
- Implementing steps with the $opinionatedElement converter
- Using step aliases
- Mapping labels to selectors
- Making assertions
- The steps library
- Given steps
- When steps
- Then steps
- Pause
- Debugger
- Current URL
- Current URL pathname
- Query param presence
- Query param presence value
- Element existence
- Element visibility
- Element text
- Element text (multiline)
- Element value
- State of checkbox or radio button
- State of checkbox or radio button corresponding to the label with given text
- Element HTML class
- Element HTML attr
- Mirage attr value
- Compare local storage value to a string (single line)
- Compare local storage value to a string (multiline)
- Assert local storage value is deeply equal to a JSON
- Assert local storage value is a subset of a JSON
- Assert local storage value is a superset of a JSON
- Local storage key existence
- Local storage emptiness
- ember-power-select steps
- ember-power-datepicker steps
- In integration tests
- Development
- Contributing
- License
As of 2019, this addon is in active development. See the first release milestone to track progress.
The addon works well with QUnit. Mocha is also supported, but individual steps will not be logged to the reporter output. This issue in ember-mocha
is blocking it. If you want to see individual steps in your Mocha output, please report to that issue.
Cucumber is a technology that has always had mixed opinions. It has lots of devoted fans, but it also has many haters, as well as people who have adopted it with enthusiasm but then got disappointed and regretted the decision.
As with many things in life, Cucumber is a tool that can give joy or grief, depending on how you use it.
One major reason for getting disappointed in Cucumber is the burden of maintaining a huge library of step implementations:
-
In a matured project, there are hundreds, sometimes thousands of steps. As the step library grows, it becomes birtually impossible to know it and use it efficiently.
-
As a consequence, duplicate steps are sometimes introduced.
-
At certain point, some steps may be no longer used and become dead code.
-
There is no simple, automated to look up a step implementation for a given step name. As far as we know, no IDE and no Cucumber implementation has a tool for this. (If you know a such tool for Yadda, please start an issue in this repo and share.)
-
Many step implementations are repetitous: they do essentially same things with slight variations. There is a lot of code duplication.
-
As you write new tests, you have to implement more and more steps, which makes writing tests very slow.
-
Step implementations obscure the truth. Step names in feature files essentially behave as references to step implementations. There is no guarantee that a step implementation will do what its name suggests. This may result in false positives.
-
The problem of obscured truth is especially tough with seeding steps. Many acceptance tests require very elaborate setups, which are impossible to express with a single phrase.
-
Seeding with a sequence of steps is an option, but a typical problem here is that such steps implicitly depend on each other's internal implementation, not exposed in feature files: cross-references with ids, amount of child records, values of attributes -- are implied.
-
Assertion steps then expect certain amounts and attribute values, even though they are not obvious from the feature file, e. g.:
Given 5 posts with comments Then the author of 3rd comment under 2nd post should be Alice
It is impossible to validate (ensure correctness of) this feature file by reading it. You have to look into actual step implementations.
This code sample is exagerrated and maybe even ridiculous. But every developer who dared to convert an acceptance test suite of a large project to Cucumber has surely stumbled into this problem.
We believe we know a radical solution to all of these problems: do not obscure any truth in step implementations, instead expose all truth in feature files (step names).
There are only so much things a user can do on the page. There are maybe a couple dozens of common things to do: see something, click something, type text into something; and a few dozen of less common (but still fully reusable) ones, such as select items in a multi-select dropdown.
So instead of doing this:
Then the save button should be disabled
we suggest that you do this:
Then the SaveButton should have attribute `aria-disabled`
Here SaveButton
will be converted to a [data-test-save-button]
test selector, and aria-disabled
is an actual HTML attribute name to be looked upon the referenced element.
A similar thing can be done with seeding. Instead of doing:
Given 5 posts with comments
...be explicit:
Given the following User records:
-------------------------------------------
| Id | Name | Role |
| @frankie | "Frankie Foster" | "admin" |
| @cheese | "Cheese" | "user" |
-------------------------------------------
And the following Post records:
-----------------------------------------------------
| Id | Body | Slug | Author |
| @1 | "I'm 22!" | "my-age" | @frankie |
| @2 | "I like cereal" | "i-like-cereal" | @cheese |
-----------------------------------------------------
And the following Comment records:
------------------------------------------------------------
| Id | Post | Author | Body |
| @1 | @1 | @cheese | "I like chocolate milk" |
| @2 | @2 | @frankie | "YOU DON'T LIVE HERE! GO HOOOME!" |
| @3 | @2 | @cheese | "Television tastes funny" |
------------------------------------------------------------
Now the feature file is explicit about what's going on in the database. The feature is unambiguous!
And we'll provide step implementations and tools to streamline these stems!
In the best case, all steps from the latter feature sample can be served by a single step implementation. This step implementation will look up Mirage models and set up relationships automatically. Remaining properties will be applied as attributes.
In the worst case, you'll have to create one step per model. In such a step, you provide a contract -- a mapping between keys/values from feature files to keys/values in the database. There you can:
- Customize attribute names: use meaningful, human-friendly names in feature files and have them automatically converted to actual attributes.
- Cast types, create non-primitive values, lookup relationships by criteria, etc.
- Expand a single source value to a certain combination of attribute values.
Note that steps don't need to rely on context (this.ctx
), since they can cross-reference each other with ids and types.
Let's start with cons.
The suggested approach:
- Makes feature files more explicit and more techinical.
- As a result, features become harder to read, especially for non-developers.
This is an inevitable price you have to pay for the compromise. The other side of the coin.
Is it worth it?
By paying this price, you get the following pros:
- A compact library of fully reusable steps:
- Easy to learn.
- Easy to navigate thanks to documentation available here.
- Little to no maintenance.
- You no longer need to have a steps file for each acceptance test. Why bother, when all steps are fully reusable across all acceptance tests?
- As you're making new tests:
- You don't need to code step implementations.
- Writing new tests is fast and enjoyable.
- You can expand existing features with more cases by simply copying the first case and adjusting the options. Not that you weren't able to do this before, but now it's even easier.
- The truth is no longer hidden inside the black box of step implementations:
- It is now possible to reliable validate features: by reading them, ensure they are making correct assertions in correct connditions.
- No false positives caused by black-boxed logic.
- Steps can cross-reference records with ids. As a result, they no longer need to rely on context (
this.ctx
), making the implementation less tangled.
As explained above, we want to move the truth from step implementations to feature files. A great way to do it is with test selectors.
To learn about test selectors, please see the ember-test-selectors addon. This addon is not required but it may be useful.
ember-cli-yadda-opinonated
introduces labels: a DSL to reference page elements via test selectors.
In its simplest form, a label can look like: Menu-Item
, MenuItem
or menu-item
. Any of these lables translates to [data-test-menu-item]
.
You can also use articles: a Menu-Item
and the Menu-Item
behave identically to Menu-Item
.
You can nest labels using prepositions in
, inside
, under
, of
, on
and from
.
For example, Save-Button in Post-Edit-Form
produces selector [data-test-post-edit-form] [data-test-save-button]
.
You can nest selectors up to 5 levels deep.
If you have multiple elements on the same page and you want to target one of them by index, you can use index prefixes like: 1st
, 2nd
, 3rd
, 4th
, 543rd
, etc. Since we're using a natural language, they are one-indexed.
Literal indices from first
to tenth
are also available.
A label 2nd Menu-Item
, the 2nd Menu-Item
, second Menu-Item
or the second Menu-Item
produces a selector [data-test-menu-item]:eq(1)
internally.
:eq(n)
is not a standard selector. We use it as an equivalent to array[n]
in JS.
So [data-test-menu-item]:eq(1)
will find all menu items on the page, then pick the second one.
Note that this is totally different from :nth-child(n)
!
Here's a more complicated example:
Link in 2nd Menu-Item from the 1st Menu
This will find the first menu, in that menu it will take the second menu item, and in that menu item it will take a link.
Each element you want to target should have one semantic label.
In addition to a semantic label, you might want to add additional labels. Here are two common cases:
- Distinguishing sibling items. E. g. each menu item may have a unique label such as
Home
,Products
,About
,Contacts
, etc. - Tracking state:
Active
,Expanded
, etc.
Use +
to compose multiple labels.
E. g. you can target an active menu item with Active+MenuItem
. This will translate to [data-test-menu-item][data-test-active]
.
Note that you can use the label map to map the Active
label to the .active
selector. If you do, Active+MenuItem
will translate to [data-test-menu-item].active
.
Yadda uses method chains to register step implementations. Here's an example from the ember-cli-yadda
readme:
import steps from 'my-app/tests/acceptance/steps/steps';
export default function(assert) {
return steps(assert)
.given('it\'s next to apples', function() {
let apples = this.element.querySelectorAll('.apple');
assert.ok(apples.length > 0)
})
.when('left together for a while', function(next) {
// bananas rot really quickly next to apples.
setTimeout(next, 1000);
})
.then('the banana rots', function () {
let banana = this.element.querySelector('.banana');
assert.ok(banana.classList.contains('rotten'));
});
}
This approach has two disadvantages:
-
It's quite boilerplatey, has a lot of visual noise.
-
It does not allow step composition. You can only inherit one file from another (similar to JS class inheritance), whereas assembling a library from multiple sources would be handy (similar to mixins).
For example, you have some generic
steps.js
and then you have acceptance-test specificlogin-steps.js
. According toember-cli-yadda
stadnard,login-steps.js
inherits fromsteps.js
.Now imagine you're building some other accepntance test with
post-steps.js
that also inherits fromsteps.js
. You have a use case where an anonymous user must see a post without edit button, then login and see the edit button. You have the login step already implemented inlogin-steps.js
, so you inheritpost-steps.js
fromlogin-steps.js
.Now Yadda will complain about conflicting steps and refuse to start:
steps.js
was inherited twice.You can work around this by not inheriting from
steps.js
. This works, but only until you want to borrow some more steps from another step file: if two step files inherit fromsteps.js
, you can't inherit from both! :(The only solution is to move all shared steps to the generic
steps.js
. Eventually it becomes large, mixed up, hard to read and to maintain. -
When an assertion error is thrown, the error message does not contain details such as step name, unless you provide the details manually for each individual assertion.
ember-cli-yadda-opinionated
lets you store steps in simple objects with methods. The above example can be rewritten like this:
// tests/acceptance/steps/_fruit-steps.js
export default {
"Given it\'s next to apples" (assert) {
let apples = this.element.querySelectorAll('.apple');
assert.ok(apples.length > 0)
},
'When left together for a while', function() {
// next() is not available. Return a promise or use async/await.
return new Promise(resolve => setTimeout(resolve, 1000));
}),
'Then the banana rots', function () {
let banana = this.element.querySelector('.banana');
assert.ok(banana.classList.contains('rotten'));
},
};
This is cleaner and easier to read.
You can compose such steps with the composeSteps
helper:
// tests/acceptance/steps/post-steps.js
import {
composeSteps, // The helper
opinonatedSteps // The step library
} from 'ember-cli-yadda-opinionated/test-support';
// Step implementations
import library from 'my-app/tests/acceptance/steps'; // A steps library, can be empty, but initialized with converters, etc
import loginSteps from 'my-app/tests/acceptance/login-steps'; // Hand-written steps from the previous example
export default composeSteps(library, opinonatedSteps, loginSteps);
This way, you can compose steps from multiple sources.
Conflicting step names will not produce an unnecessary crash, letting you manually control the composition: the last conflicting step overwrites previous ones.
This also lets you run one step from another:
export default const steps = {
// A one-liner step to seed a single post
"Given a post with $fields"(fields) {
/* seeding */
},
// A step that uses table syntax: useful to seed multiple records,
// but not practical to seed just one, since it consumes at least four lines of feature file
"Given the following posts with\n$opinionatedTable"(rows) {
rows.forEach(row => {
steps["Given a post with $fields"].call(this, row);
})
},
};
As for error messages thrown from inside composed steps, ember-cli-yadda-opinionated
enriches them with details automatically:
- Step name as it appears in the feature file.
- Matched step name as it appears in step implementation*
- Arguments passed into the step implementation. For labels, extra details are provided: label, resulting selector and found elements count.
-
Make sure you have `ember-cli-yadda installed.
-
Install the addon and its dependencies:
ember install ember-cli-yadda-opinionated ember install ember-cli-chai npm install -D chai-dom # or: yarn add -D chai-dom
-
Add the hooks.
In your app's
tests/helpers/yadda-annotations.js
file, import the setup helper:import { setupYaddaOpinionated } from 'ember-cli-yadda-opinionated/test-support';
Find this fragment:
if (annotations.setupapplicationtest) { return setupApplicationTest; }
And replace it with this:
if (annotations.setupapplicationtest) { return function() { const hooks = setupApplicationTest(); setupYaddaOpinionated(hooks); }; }
-
Extend your dictionary.
In your app's
tests/acceptance/steps/steps.js
file, you should have something like this:new yadda.Dictionary() .define('number', /(\d+)/, yadda.converters.integer) .define('table', /([^\u0000]*)/, yadda.converters.table);
Extend it like this:
import { setupDictionary } from 'ember-cli-yadda-opinionated/test-support'; const dictionary = new yadda.Dictionary() .define('number', /(\d+)/, yadda.converters.integer) .define('table', /([^\u0000]*)/, yadda.converters.table); setupDictionary(dictionary)
Note that the default table converter from Yadda is required, named
table
. -
Extend you steps. See below.
According to ember-cli-yadda
, you are supposed to organize steps like this:
-
Primary
steps.js
Located at
tests/acceptance/steps/steps.js
. In this file, you:- Initialize a Yadda dictionary with converters.
- Optionally, bind to Yadda events (though this can also be done in
tests/test-helper.js
). - Implement steps that should be shared across multiple feature files.
Though this is not strictly required, we suggest that you move all step implementations from
steps.js
to composable step definitions (see below). -
Per-feature steps files:
<feature>-steps.js
Located at
tests/acceptance/steps/<feature-name>-steps.js
.You must have such file for each feature file, otherwise tests won't start.
In these files, you are supposed to implement steps that are only relevant to one feature.
As explained in the rationale section, this approach is troublesome because very often you want to use steps from one feature in another feature. The only way to do it with ember-cli-yadda
is to move those step implementations to the primary steps.js
, preventing you from organizing step implementations into files by topic.
ember-cli-yadda-opinionated
lets you organize steps by topic, not directly bound to a specific feature. For example, you can have a collection of authentication steps. You can use these steps in a login-and-logout.feature
, but in addition to that you can use them in any other features that need logging in as part of user stories. Say, in an admin-panel.feature
you want to test that the access to the admin panel is restricted once the user logs out.
We suggest that you organize such composable steps in files prefixed with an underscore (file contents is explained below):
tests/acceptances/steps/_authentication-steps.js
tests/acceptances/steps/_side-menu-steps.js
The underscore would distinguish composable steps from ember-cli-yadda
's step fiels..
We also recommend that you move all existing step implementations from steps.js
and <feature>-steps.js
into composable step files.
If you do so, the steps.js
file should export an empty Yadda step library factory:
export default function() {
return yadda.localisation.default.library(dictionary);
}
ember-cli-yadda-opinionated
uses a simple syntax for defining step implementations.
Each composable step file, e. g. _authentication-steps.js
, should export a simple object with methods:
export default {
'Given I am logged in as a $role'(role) {
/* ... */
},
}
The name of each method is a step name pattern.
The pattern should start with Given
, When
, Then
or Define
.
The name can contain converter macros, e. g. $role
, and regular expressions. In fact, the whole step name is used a regular expression.
Yadda adds ^
to the beginning of the regex, and ember-cli-yadda-opinionated
also terminates it with a $
.
Don't forget that you must use double backslashes for escaping, e. g. (\\d+)
. Single backslashes are swallowed by the string.
ember-cli-yadda
still requires that you have one individual step file for each accepatnce test, e. g.:
tests/acceptance/steps/login-logout-steps.js
In that file, you normally would normally have:
import steps from 'ember-cli-yadda-opinionated/test-support/-private/steps';
export default function() {
return steps()
.given('I am logged in as a $role', function(role) {
/* ... */
})
}
Replace it with:
import libraryFactory from 'ember-cli-yadda-opinionated/test-support/-private/steps';
import { composeSteps, opinonatedSteps } from 'ember-cli-yadda-opinionated/test-support';
import authenticationSteps from '<my-app>/tests/acceptance/steps/_authentication-steps';
export default composeSteps(libraryFactory, opinonatedSteps, authenticationSteps);
composeSteps
accepts the Yadda step library factory (from steps.js
) as the first argument. Other arguments are composable step files that you want to be used with this feature.
Note that <feature>-steps.js
files no longer contain step implementations. Instead, for each feature, you compose steps from reusable composable step files.
This way you can keep your step implementations organized by topic. For each acceptance test, you can include the ones you need.
If you make your steps fully reusable and not concealing any truth (see rationale), then you can include all your steps for every feature. This way, all individual <feature>-steps.js
files for your acceptance tests will be identical.
Otherwise, composable steps behave as normal Yadda steps defined with the chained syntax .given().when().then()
. If you return a promise from a step, the step will become async. You can also make a step async by using the async/await
syntax.
The $opinionatedElement
converter accepts an ember-cli-yadda-opinionated
label (see above) and returns an array of matching elements.
A step pattern When I click $opinionatedElement
will match the following step names:
When I click Button
When I click a Button
When I click the 2nd Button in the Post-Edit-Form of the Active+Post
In each case, $opinionatedElement
will resolve with a collection of matched buttons.
a Button
. When only a single element is matched, the array will contain one element. If none are matched, the array will be empty.
The exact return value of $opinionatedElement
is [collection, label, selector]
. It is a tuple (an array with fixed number of elements) that contains:
- 0:
collection
: an array with matched elements. - 1:
label
: the label used in the step name, useful for debugging. - 2:
selector
: the selector that the label was converted to, useful for debugging.
In its simplest form, a clicking step could be implemented like this:
import {click} from `@ember/test-helpers`;
export {
"When I click $opinionatedElement"([collection]) {
const element = collection[0];
return click(element);
}
}
But this step will crash with a cryptic error in case the label didn't match an element. And if the element matched multiple elements, the outcome of the step may be unpredictable and hard to debug.
To resolve this issue, you should guard against it:
import {click} from `@ember/test-helpers`;
export {
"When I click $opinionatedElement"([collection, label, selector]) {
assert(
`Expected a single element, but ${collection.length} found.\nLabel: ${label}\nSelector: ${selector}\nStep: ${this.step}`,
collection.length === 1
);
return click(collection[0]);
}
}
Sometimes it's useful to provide two different phrasings for the same step. Very often the prhasing are too different to cover them with a single regular expression.
You can resolve this by referencing one step from another like this:
import {click} from `@ember/test-helpers`;
export {
"When I click $opinionatedElement"([collection, label, selector]) {
assert(
`Expected a single element, but ${collection.length} found.\nLabel: ${label}\nSelector: ${selector}\nStep: ${this.step}`,
collection.length === 1
);
return click(collection[0]);
},
"When $opinionatedElement is clicked": "When I click $opinionatedElement"
}
Sometimes you want your tests to operate on page elements produced by a third-party addon or library. You don't have control over its HTML output and thus can't sprinkle it with test selectors.
To resolve the issue, you can provide a mapping of labels to selectors. Do this in tests/acceptanse/steps/steps.js
or tests/test-helper.js
:
import { labelMap } from 'ember-cli-yadda-opinionated';
labelMap.set('Bootstrap-Text-Input', 'input.form-control[type="text"]');
labelMap.set('Bootstrap-Textarea', 'textarea.form-control');
These labels will be automatically converted to selectors (case-sensitive).
You should consider scoping those selectors with a library's unique HTML class, if available.
Steps use Mocha-style assertions: if no error has been thrown, the step is assumed to be successful. If your app is using QUnit, a successful assertion will be logged with the step name.
When an error is thrown, its message will be used as an assertion error.
This enables using assertion libraries like Chai.
Assertion/error messages are enriched with additional details:
- 👟 Step name
- ⚙ The name of the step implementation used (useful to make sure Yadda picked the correct implementation)
- ⚠ Assertion or error message thrown
- 🛠 List of arguments passed into the step
ember-cli-yadda-opinionated
aims to offer an extensive and universal steps library. Steps are organized into three composable modules: given, when and then. See Composing steps to find out how to them.
ember-cli-yadda-opinionated
provides a generic step to seed Mirage records of any type. It covers basic cases.
For advanced cases, we recommend to implement one custom seeding per each model (see below).
Import:
import { givenSteps } from 'ember-cli-yadda-opinionated/test-support';
Sets server.logging
to true in Mirage until end of test.
Signature: Given server.logging
It will simply pass provided properties and traits as-is to Mirage's server.createList()
, with the following nuances:
-
The model name will be camelCased.
-
Property names and values are used as-is.
-
Relationships will be automatically recognized by inspecting property types on the corresponding Mirage model.
Values must be ids, without quotes, prefixed with
@
, e. g.@1
or@foo
.For a to-many relationship, use plural key and delimit ids with commas. This way you can populate one-to-one and one-to-many relationships (from the one side).
Alternatively, you can use Mirage's default behavior and pass ids, e. g.
{"comment_ids": [1, 2]}
.For polymorphic relationships, you can optionally provide a type of each related record next to the id in parens:
@1(User), @2(Bot)
. You can also provide the default type in the key, e. g."authors(User)": "@1, @3)
. Both approaches can be mixed, the per-id type has priority. If the type is provided neither in key nor in id, then the base type of the polymorphic relationship will be used to create the related record.
Signature: Given there(?: is a|'s a| are|'re) (?:(\\d+) )?records? of type (\\w+)(?: with)?(?: traits? (.+?))?(?: and)?(?: propert(?:y|ies) ({.+?}))?
Examples:
Given there is a record of type Post
Given there's a record of type Post
Given there is a record of type Post with property {"id": "1"}
Given there is a record of type Post with properties {"id": "1", "title": "Foo", "author": "@mike"}
Given there is a record of type Post with properties {"id": "1", "title": "Foo", "author(User)": "@mike"}
Given there is a record of type Post with properties {"id": "1", "title": "Foo", "author": "@mike(User)"}
Given there is a record of type Post with properties {"id": "1", "title": "Foo", "authors": "@mike(User), @bob(Bot)"}
Given there is a record of type Post with properties {"id": "1", "title": "Foo", "authors(User)": "@mike, @bob(Bot)"}
Given there are 2 records of type Post with trait published
Given there is a record of type Post with traits published, pinned and commented
Given there is a record of type Post with traits published and commented and properties {"id": "1", "title": "Foo"}
Invalid example:
# Two records of the same type can't have the same id
Given there are 2 records of type Post with properties {"id": "1", "title": "Foo", author: "@mike"}
Though this step is similar to the above, it has slightly different behavior:
-
The model name will be camelCased.
-
Keys (column headers) and values are trimmed.
-
Property names are used as-is, except for names
trait
andtraits
, which are used for traits. -
Relationships will be automatically recognized by inspecting property types on the corresponding Mirage model.
Values must be ids, without quotes, prefixed with
@
, e. g.@1
or@foo
.For a to-many relationship, use plural key and delimit ids with commas. This way you can populate one-to-one and one-to-many relationships (from the one side).
Alternatively, you can use Mirage's default behavior and pass ids, e. g.
{"comment_ids": [1, 2]}
.For polymorphic relationships, you can optionally provide a type of each related record next to the id in parens:
@1(User), @2(Bot)
. You can also provide the default type in the key, e. g."authors(User)": "@1, @3)
. Both approaches can be mixed, the per-id type has priority. If the type is provided neither in key nor in id, then the base type of the polymorphic relationship will be used to create the related record. -
Empty cells are treated as
null
. -
Other values are parsed as JSON. Note that strings, numbers, booleans and
null
are JSON entries in their full right. :)This means that you must wrap strings in double quotes.
Signature: Given there are records of type (\\w+) with the following properties:\n$opinionatedTable
Examples:
Given there are records of type User with the following properties:
----------------------------------------
| id | name | trait |
| "bloo" | "Blooregard Q. Kazoo" | admin |
| "wilt" | "Wilt" | user |
--------------------------------------
And there are records of type Post with the following properties:
---------------------------------
| id | title | author |
| 1 | "Hello, World!" | @bloo |
| 2 | "Foo Bar Baz" | @wilt |
---------------------------------
And there are records of type Post with the following properties:
---------------------------------------
| id | title | author(User) |
| 1 | "Hello, World!" | @bloo |
| 2 | "Foo Bar Baz" | @wilt |
| 3 | "Zomg Lol Quux" | @cheese(Bot) |
---------------------------------------
Useful for testing error states.
Signature: Given there is a $opinionatedInteger error for the API (.+) call to "(.+)"
Examples:
Given there is a 500 error for the API POST call to "/posts"
Helps testing the app in different build modes.
Signature: Given configuration property "(.+?)" is set to $opinionatedJSON
Examples:
Given configuration property "theme" is set to "dark"
Signature: Given local storage key $opinionatedString is set to $opinionatedString
Value is a string which can contain JSON or anything else.
Allows escaping double quotes.
Example:
Given local storage key "my-app-config" is set to "foo"
Given local storage key "my-app-config" is set to "{\\"id\\": \\"foo\\"}"
Signature: Given local storage key $opinionatedString is set to the following value:\n$opinionatedText
Value is a string which can contain JSON or anything else.
Example:
Given local storage key "my-app-config" is set to the following value:
-----------------------------------------
{
"id": "foo",
"createdAt": "2019-11-21T08:28:59.973Z"
}
-----------------------------------------
Signature: Given local storage key $opinionatedString does not exist
Value is a string which can contain JSON or anything else.
Example:
Given local storage key "my-app-config" does not exist
Signature: Given local storage is empty
Example:
Given local storage is empty
Import:
import { whenSteps } from 'ember-cli-yadda-opinionated/test-support';
Implements visit()
from @ember/test-helpers
.
Signature: When I (?:visit|am at|proceed to) URL (.*)
.
Examples:
When I visit URL /login
When I am at URL /products/1
When I proceed to URL /
Implements await settled()
from @ember/test-helpers
.
Signature: When the app settles
.
Example: When the app settles
Implements click()
from @ember/test-helpers
.
Will crash if no elements or more than one element matched.
Signature: When I click (?:on )?$opinionated$opinionatedElement
.
Examples:
When I click the Submit-Button
When I click on the 2nd Menu-Item in the Navigation-Menu
Implements doubleClick()
from @ember/test-helpers
.
Will crash if no elements or more than one element matched.
Signature: When I double click (?:on )?$opinionatedElement
.
Examples:
When I double click the Submit-Button
When I double click on the 2nd Menu-Item in the Navigation-Menu
Implements fillIn()
from @ember/test-helpers
.
Will crash if no elements or more than one element matched.
If the referenced element is not fillable, a fillable element will be looked up inside the referenced element.
Signature: When I fill \"(.*)\" into $opinionatedElement
.
Example: When I fill "cheese" into the Username-Field
Triggers the mouseenter
event on the element.
Will crash if no elements or more than one element matched.
Signature: When I move the mouse pointer into $opinionatedElement
.
Example: When I move the mouse pointer into the Edit-Button
Triggers the mouseleave
event on the element.
Will crash if no elements or more than one element matched.
Signature: When I move the mouse pointer out of $opinionatedElement
.
Example: When I move the mouse pointer out of the Edit-Button
.
Selects or deselects given checkbox/radio.
The referenced element can be either an input or contain exactly one input.
Will crash if the input is already in the desired state.
A radio button cannot be deselected.
Signature: When I (de)?select (?:the )?(?:radio button|checkbox) in $opinionatedElement
Expamples:
When I select checkbox I-Am-Not-A-Robot-Field
When I the select checkbox I-Am-Not-A-Robot-Field
When I deselect checkbox I-Am-Not-A-Robot-Field
When I deselect the checkbox I-Am-Not-A-Robot-Field
When I select radio-button Prefer-Not-To-Tell
When I select the radio-button Prefer-Not-To-Tell
First, a <label>
containing given text is looked up inside the given element.
Then a checkbox/radio is found that is associated with the label.
The checkbox/radio must either be inside the label or be refernced via the for
attribute.
Will crash if more than one input exists inside the label.
Will crash if the input is already in the desired state.
A radio button cannot be deselected.
Signature: When I (de)?select (?:the )?(?:radio button|checkbox) \"(.+?)\" in $opinionatedElement
Expamples:
When I select checkbox "I am not a robot" in the Sign-Up-Form
When I the select checkbox "I am not a robot" in the Sign-Up-Form
When I deselect checkbox "I am not a robot" in the Sign-Up-Form
When I deselect the checkbox "I am not a robot" in the Sign-Up-Form
When I select radio button "Prefer not to tell" in the Gender-Field
When I select the radio button "Prefer not to tell" in the Gender-Field
Import:
import { thenSteps } from 'ember-cli-yadda-opinionated/test-support';
When used without an argument, implements pauseTest()
from @ember/test-helpers
.
When used with a number, waits for given number of milliseconds, then waits for settled state.
Useful for debugging and waiting for processess that are not respected by settled()
. Please note that doing so makes your tests slow and brittle. Instead, you should try making settled()
aware of pending processes.
Signature: Then pause(?: for ?(\\d+) ms)?
Examples:
Then pause
Then pause for 50 ms
Implements debugger()
. Useful for, you guessed it, debugging. :)
Signature: Then debug(?:ger)?
Examples:
Then debug
Then debugger
Checks the currentURL()
to be an exact match of given URL.
Signature: Then I should (?:still )?be (?:at|on) URL (.*)
.
Example:
Then I should be at URL /about
Then I should be on URL /products?expand=true
Then I still should be at URL /products/1
Then I still should be on URL /
Checks the currentURL()
pathname (without query params and hash) to be an exact match of given URL.
Signature: Then current URL's pathname should be (.+)
.
Example:
Then current URL's pathname should be /about
Then current URL's pathname should be /
Incorret usage:
Then current URL's pathname should be /products?expand=true
Checks the currentURL()
contains or does not contain the specified query param.
Signature: Then current URL should (not|NOT)? ?have query param \"(\\w+)\"
.
Example:
Then current URL should have query param "expand"
Then current URL should NOT have query param "utm_source"
Checks the currentURL()
contains a query param with or without given value.
Note: produces positive result when a query param is not present and the step is used with NOT
.
Signature: Then current URL should (not|NOT)? ?have query param \"(\\w+)\" with value \"(.*)\"
.
Example:
Then current URL should have query param "expand" with value "true"
Then current URL should NOT have query param "page" with value "1"
Checks for exactly the specified amount of given element(s) to exist in the DOM.
If number is not provided, it defaults to 1
.
If NO
is provided, the number is set to 0
.
Signature: Then there should be (NO |no )?(?:(\\d+) )?$opinionatedElement
.
Example:
Then there should be an Error-Message
Then there should be 2 Posts
Then there should be NO Comments
Invalid usage:
Then there should be NO 2 Comments
Checks that exactly the specified amount of given element(s) exist in the DOM and all of them are visible.
If number is not provided, it defaults to 1
.
If NOT
is provided, the number is set to 0
.
Signatures:
Then (?:(\\d+) )?$opinionatedElement should (not|NOT)? ?be visible
Then I should see (NO |no )?(?:(\\d+) )?$opinionatedElementke
Example:
Then an Error-Message should be visible
Then 5 Error-Messages should be visible
Then NO Error-Messages should be visible
Then I should see a Post
Then I should see 2 Posts
Then I should see NO Posts
Invalid usage:
Then NO 2 Error-Messages should be visible
Then I should see NO 5 Posts
Checks if given element's trimmed text is or is not equal to the given text.
Will crash if no elements or more than one element matched.
If an element is an input or a textarea, its value
attribute will be checked instead. It will not be trimmed.
Signature: Then $opinionatedElement should (NOT | not )?(?:have text|say|be) \"(.*)\"
.
Example:
Then the Error-Message should have text "Something went wrong!"
Then the Error-Message should NOT have text "Something went wrong!"
Then the Title of 1st Post should say "Hello, World!"
Then the Title of 1st Post should NOT say "Hello, World!"
Then the Quantity of the Cart should be "2"
Then the Quantity of the Cart should NOT be "2"
Checks if given element's trimmed text is or is not equal to the given multiline text.
Will crash if no elements or more than one element matched.
If an element is an input or a textarea, its value
attribute will be checked instead. It will not be trimmed.
Signature: Then $opinionatedElement should (not|NOT)? ?have the following text:\n$opinionatedText
.
Example:
Then the Error-Message should have the following text:
------------
System Error
Networ down
------------
Then the Error-Message should NOT have the following text:
------------
System Error
Networ down
------------
Checks if given element's value is equal to the given text.
Will crash if no elements or more than one element matched, except when NOT is passed.
If the referenced element is not editable, then an editable element will be looked up inside the referenced element. Exactly one input/textarea/select is expected to be found inside the given element.
Signature: Then $opinionatedElement should (not|NOT)? ?have value \"(.*)\"
.
Example:
Then the Description-Field should say "Hello, World!"
Then the Description-Field should NOT say "Hello, World!"
Checks the state of a given checkbox/radio.
The referenced element can be either an input or contain exactly one input.
Will crash if no elements or more than one element matched, except when NOT is passed.
Signature: Then (?:the )?(?:radio button|checkbox) $opinionatedElement should (not|NOT)? ?be selected
Expamples:
Then checkbox I-Am-Not-A-Robot-Field should be selected
Then the checkbox I-Am-Not-A-Robot-Field should be selected
Then checkbox I-Am-Not-A-Robot-Field should NOT be selected
Then the checkbox I-Am-Not-A-Robot-Field should NOT be selected
Then radio button Prefer-Not-To-Tell should be selected
Then the radio button Prefer-Not-To-Tell should be selected
Then radio button Prefer-Not-To-Tell should NOT be selected
Then the radio button Prefer-Not-To-Tell should NOT be selected
Checks the state of checkbox/radio that is associated with a label containing provided text.
First, a <label>
containing given text is looked up inside the given element.
Then a checkbox/radio is found that is associated with the label.
The checkbox/radio must either be inside the label or be refernced via the for
attribute.
Will crash if no elements or more than one element matched, except when NOT is passed.
Will crash if more than one input exists inside the label.
Signature: Then (?:the )?(?:radio button|checkbox) \"(.+?)\" should (not|NOT)? ?be selected in $opinionatedElement
Expamples:
Then checkbox "I am not a robot" in the Sign-Up-Form should be selected
Then the checkbox "I am not a robot" in the Sign-Up-Form should be selected
Then checkbox "I am not a robot" in the Sign-Up-Form should NOT be selected
Then the checkbox "I am not a robot" in the Sign-Up-Form should NOT be selected
Then radio-button "Prefer not to tell" in the Gender-Field should be selected
Then the radio-button "Prefer not to tell" in the Gender-Field should be selected
Then radio-button "Prefer not to tell" in the Gender-Field should NOT be selected
Then the radio-button "Prefer not to tell" in the Gender-Field should NOT be selected
Checks if given element has given HTML class.
Will crash if no elements or more than one element matched, except when NOT is passed.
Signature: Then $opinionatedElement should have (not|NOT)? ?HTML class \"(.*)\"
.
Example:
Then the second Menu-Item should have HTML class "active"
Then the second Menu-Item should NOT have HTML class "active"
Checks if given element has given HTML attr. Optionally checks the attr to match given value.
Will crash if no elements or more than one element matched, except when NOT is passed.
Signature: Then $opinionatedElement should (not|NOT)? ?have HTML attr \"(.*)\"(?: with value \"(.+?)\")?
.
Example:
Then the second Menu-Item should have HTML attr "href"
Then the second Menu-Item should have HTML attr "href" with value "/products"
Then the second Menu-Item should NOT have HTML attr "href"
Then the second Menu-Item should NOT have HTML attr "href" with value "/products"
Checks if given attr of a record of given type and id has given value.
Value is expected to be JSON (including string, number and null
).
Signature: Then record of type (\\w+) and id (\\w+) should have attribute (\\w+) with value (.+)
.
Example:
Then record of type Post and id 1 should have attribute authorId with value "alice1"
Signature: Then local storage value for $opinionatedString should (not |NOT )be equal to $opinionatedString
Value is a string which can contain JSON or anything else.
Allows escaping double quotes.
Example:
Given local storage key "my-app-config" is set to "foo"
Given local storage key "my-app-config" is set to "{\\"id\\": \\"foo\\"}"
Signature: Then local storage value for $opinionatedString should (not |NOT )be equal to the following value:\n$opinionatedText
Example:
Then local storage value for "my-app-config" should NOT be equal to the following text:
-----------
Hello world
-----------
Signature: Then local storage value for $opinionatedString should (not |NOT )be deeply equal to the following JSON:\n$opinionatedJSON
Example:
Then local storage value for "my-app-config" should be deeply equal to the following JSON:
-----------------------------------------
{
"id": "foo",
"createdAt": "2019-11-21T08:28:59.973Z"
}
-----------------------------------------
Signature: Then local storage value for $opinionatedString should (not |NOT )be a subset of the following JSON:\n$opinionatedText
Example:
Then local storage value for "my-app-config" should be a subset of the following JSON:
-----------------------------------------
{
"id": "foo",
"createdAt": "2019-11-21T08:28:59.973Z"
}
-----------------------------------------
Signature: Then local storage value for $opinionatedString should (not |NOT )be a superset of the following JSON:\n$opinionatedText
Example:
Then local storage value for "my-app-config" should be a superset of the following JSON:
-----------------------------------------
{
"id": "foo",
"createdAt": "2019-11-21T08:28:59.973Z"
}
-----------------------------------------
Signature: Then local storage key $opinionatedString should (not |NOT )?exist
Examples:
Then local storage key "my-app-config" should exist
Then local storage key "my-app-config" should not exist
Then local storage key "my-app-config" should NOT exist
Signature: Then local storage should (not |NOT )?be empty
Examples:
Then local storage should be empty
Then local storage should not be empty
Then local storage should NOT be empty
Import:
import powerSelectSteps from 'ember-cli-yadda-opinionated/test-support/steps/power-select';
Checks if a ember-power-select
contains the specified number of items.
Will crash if no elements or more than one element matched the power select.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: Then there should be (NO|no )?(?:(\\d+) )items? in the dropdown $opinionatedElement
.
Example:
Then there should be NO items in the dropdown Pet
Then there should be 1 item in the dropdown Pet
Then there should be 2 items in the dropdown Pet
Checks if a ember-power-select
contains the specified number of selected items (in multiple mode).
Will crash if no elements or more than one element matched the power select.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: Then there should be (NO|no )?(?:(\\d+) )items? in the dropdown $opinionatedElement
.
Example:
Then there should be NO selected items in the dropdown Pet
Then there should be 1 selected item in the dropdown Pet
Then there should be 2 selected items in the dropdown Pet
Checks if an ember-power-select
trigger contains the specified trimmed text.
Will crash if no elements or more than one element matched the power select.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: Then the dropdown $opinionatedElement should have \"(.*)\" selected
.
Example:
Then the dropdown Pet should have "Rex" selected
Checks if Nth item in an ember-power-select
contains the specified trimmed text.
Will crash if no elements or more than one element matched the power select.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: Then (?:(?:a|an|the) )?(?:(\\d+)(?:st|nd|rd|th) |(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth) )?item in the dropdown $opinionatedElement should (not|NOT)? ?(?:have text|say|be) \"(.*)\"
.
Example:
Then the first item in the dropdown Pet should say "Tom"
Then the first item in the dropdown Pet should be "Tom"
# Checks against the first item. Does not assert the amount of items.
Then the item in the dropdown Pet should have text "Rex"
Clicks Nth item in an ember-power-select
.
Will crash if no elements or more than one element matched the power select.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: When I select (?:(?:a|an|the) )?(?:(\\d+)(?:st|nd|rd|th) |(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth) )?item in the dropdown $opinionatedElement
.
Example:
When I select the second item in the dropdown Pet
# Checks against the first item. Does not assert the amount of items.
When I select the item in the dropdown Pet
Clicks an item in an ember-power-select
selected by text content or selector provided.
Will crash if no elements or more than one element matched the power select.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: When I select (?:(?:a|an|the) )?(?:(\\d+)(?:st|nd|rd|th) |(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth) )?item \"(.+)\" in the dropdown $opinionatedElement
.
Example:
When I select item "Rex" in the dropdown Pet
# In case multiple elements with the same name exist
When I select first item "Rex" in the dropdown Pet
Clicks Nth selected item in an ember-power-select
.
Will crash if no elements or more than one element matched the power select.
Will crash if the number provided is larger than the number of selected items.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: When I deselect (?:(?:a|an|the) )?(?:(\\d+)(?:st|nd|rd|th) |(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth) )?selected item in the dropdown $opinionatedElement
.
Example:
When I select the second item in the dropdown Pet
# Checks against the first item. Does not assert the amount of items.
When I select the item in the dropdown Pet
Clicks a selected item's remove button in an ember-power-select
. The selected item is found by text content.
Will crash if no elements or more than one element matched the power select.
Will crash if the number provided is larger than the number of selected items having given text.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: When I deselect (?:(?:a|an|the) )?(?:(\\d+)(?:st|nd|rd|th) |(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth) )?selected item \"(.+)\" in the dropdown $opinionatedElement
.
Example:
When I deselect selected item "Rex" in the dropdown Pet
# In case multiple elements with the same name exist
When I deselect first selected item "Rex" in the dropdown Pet
Checks that Nth selected item in an ember-power-select
is locked.
Will crash if no elements or more than one element matched the power select.
Will crash if the number provided is larger than the number of selected items.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: Then (?:(?:a|an|the) )?(?:(\\d+)(?:st|nd|rd|th) |(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth) )?selected item in the dropdown $opinionatedElement should (not|NOT)? ?be disabled
.
Example:
Then the second selected item in the dropdown Pet should be disabled
# Checks against the first item. Does not assert the amount of items.
Then the selected item in the dropdown Pet should be disabled
Checks that a certain selected item in an ember-power-select
is locked. The selected item is found by text content.
Will crash if no elements or more than one element matched the power select.
Will crash if the number provided is larger than the number of selected items having given text.
If the referenced element is not a power select, a power select will be looked up inside the referenced element.
Signature: Then (?:(?:a|an|the) )?(?:(\\d+)(?:st|nd|rd|th) |(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth) )?selected item \"(.+)\" in the dropdown $opinionatedElement should (not|NOT)? ?be disabled
.
Example:
Then selected item "Rex" in the dropdown Pet should be disabled
# In case multiple elements with the same name exist
Then the first selected item "Rex" in the dropdown Pet should be disabled
Import:
import powerDatePickerSteps from 'ember-cli-yadda-opinionated/test-support/steps/power-date-picker';
Selects a date in a date picker. Will automatically expand the datepicker and scoll to required date, then click on it.
Accepts a date in ISO format.
If the referenced element is not a date picker, a date picker will be looked up inside the referenced element.
Signature: When I select date \"(.+)\" in the date picker $opinionatedElement
Expample:
When I select "2018-01-01" in the date picker Release-Date-Field
ember-cli-yadda-opinionated
offers a number of helpers, which are equivalents of helpers from @ember/test-helpers
:
import {
findByLabel,
clickByLabel
} from 'ember-cli-yadda-opinionated/test-support/dom-helpers;
findByLabel(label)
-- eqauivalent offindAll
, but returns a tuple[collection, label, selector]
, wherecollection
is an array of found elements. Useful for making meaningful assertion error messages.findAllByLabel(label)
-- eqauivalent offindAll
. Returns an array of found elements.findSingleByLabel(label)
-- eqauivalent offind
. Returns an element. Will crash if found more than one element or no elements.await clickByLabel(label)
-- equivalent ofclick
. Will crash if found more than one element or no elements. Is async.await doubleClickByLabel(label)
-- equivalent ofdoubleClick
. Will crash if found more than one element or no elements. Is async.fillInByLabel(label, text)
-- looks up a fillable element viafindEditable
, then appliesfillIn
to it. Will crash if found more than one element or no elements.findEditable(element, shouldIncludeContentEditable)
-- returns current element if it's editable (a non-hidden input, textarea, select or contenteditable). If not, will look up such an element inside the given element. Will crash if found more than one element or no elements. SetshouldIncludeContentEditable
tofalse
to exclude contenteditable.await triggerByLabel(label, eventName, options)
-- equivalent oftriggerEvent
. Will crash if found more than one element or no elements. Is async.await triggerKeyByLabel(label, eventName, options)
-- equivalent oftriggerEvent
. Will crash if found more than one element or no elements. Is async.await mouseEnterByLabel(label)
-- triggers themouseenter
event on the element. Will crash if found more than one element or no elements. Is async.await mouseLeaveByLabel(label)
-- triggers themouseleave
event on the element. Will crash if found more than one element or no elements. Is async.findInputForLabelWithText(text, parent)
-- finds an input on the page by searching for a label element with given text, then looking up an input that corresponds to the label. Parent is optional.findSelfOrChild(element, htmlClass)
: accepts a DOM element and an HTML class. Returns either the element or the first matching child.
powerSelectFindTriggerByLabel(label)
- find a power select inside given element or selector (including self).powerSelectFindDropdownByLabel(label)
- find a dropdown containing a list of options options corresponding to a given trigger.await powerSelectFindOptionsByLabel(label)
- find a options inside a dropdown corresponding to a given trigger. Will expand the dropdown if not already. Is async.await powerSelectSelectOptionByLabelAndIndex(label, index)
- clicks on Nth option in a power select. Is async.powerSelectFindSelectedOptionsByLabel(label)
- find selected options inside a given trigger (for multi-select dropdown).powerSelectFilterSelectedOptionsByLabelAndText(label, text)
- filter selected options by text.powerSelectIsSelectedOptionDisabledByLabelAndIndex(label, index)
- checks if given selected option is disabled (locked).powerSelectIsSelectedOptionDisabledByLabelAndText(label, text)
- checks if given selected option is disabled (locked), expects only one selected option to match text.powerSelectIsTriggerDisabledByLabel(label)
- returnstrue
when given trigger is disabled.await powerSelectRemoveSelectedOptionByLabelAndIndex(label, index)
- clicks on the remove button of the given selected option. Is async.await powerSelectRemoveSelectedOptionByLabelAndText(label, text)
- clicks on the remove button of the given selected option, expects only one selected option to match text. Is async.powerSelectIsDropdownExpandedByLabel(label)
- checks if a dropdown corresponding to given trigger is expanded.await powerSelectExpandByLabel(label)
- clicks on given trigger, unless its dropdown is already expanded. Is async.await powerSelectCollapseByLabel(label)
- clicks on given trigger, unless its dropdown is already collapsed. Is async.powerSelectFindOptionByLabelAndValue(label, valueOrSelector, optionIndex = 0)
- finds an option by text (and index, in case of multiple options with same text). Expects the dropdown to be expanded.
These helpers operate on selectors/elements rather than labels.
powerSelectFindTrigger(triggerOrSelector)
- find a power select inside given element or selector (including self).powerSelectFindDropdown(trigger)
- find a dropdown containing a list of options options corresponding to a given trigger.powerSelectFindOptions(trigger)
- find a options inside a dropdown corresponding to a given trigger. Expects the dropdown to be expanded.powerSelectFindSelectedOptions(trigger)
- find selected options inside a given trigger (for multi-select dropdown).powerSelectFilterSelectedOptionsByText(selectedOptions, text)
- filter selected options by text.powerSelectIsSelectedOptionDisabled(option)
- checks if given selected option is disabled (locked).powerSelectIsTriggerDisabled(trigger)
- returnstrue
when given trigger is disabled.powerSelectRemoveSelectedOption(option)
- clicks on the remove button of the given selected option.powerSelectIsDropdownExpanded(trigger)
- checks if a dropdown corresponding to given trigger is expanded.await powerSelectExpand(trigger)
- clicks on given trigger, unless its dropdown is already expanded. Is async.await powerSelectCollapse(trigger)
- clicks on given trigger, unless its dropdown is already collapsed. Is async.powerSelectFindOptionByValueOrSelector(trigger, valueOrSelector, optionIndex = 0)
- finds an option by text (and index, in case of multiple options with same text). Expects the dropdown to be expanded.'
powerDatePickerFindTrigger(triggerOrSelector)
- finds a power date picker inside given element (including self).powerDatePickerFindTriggerByLabel(label)
- finds a power date picker inside given element (including self).powerDatePickerFindDropdown()
- finds a power date picker dropdown (globally, does not require a selector/label).
Use Yarn.
To update the table of contents after editing the readme, run:
npx doctoc README.md --github
See the Contributing guide for details.
This project is licensed under the MIT License.