Use Yarn.
See the Contributing guide for details.
This project is licensed under the MIT License.
A radical take on Cucumber
License: MIT License
Use Yarn.
See the Contributing guide for details.
This project is licensed under the MIT License.
Currently splitting compound selectors ([data-test-foo] [data-test-bar]
) is performed like this:
const selectorsMaybeWithEq = selectorCompound.split(/\s+/);
This should be safe for normal usage of the addon, but a mapped label can have spaces and ruin it:
labelMap.set('Group-Admin-User', '[data-test-user-type="group admin"]');
Need to implement a safe way of splitting selectors. Not sure how to do it with regex, should we use some kind of parser instead?
I've already implemented the $opinionatedString
converter for a string wrapped into quotes. It allows escaping quotation marks!
We should use this converter instead of all capturing groups like (.+)
and (.+?)
.
ToDo: figure out if it's possible to use converters inside non-capturing groups. If not, existing instances of (.+)
and (.+?)
inside capturing groups should be replaced with equivalents that allow escaping quotes.
This will be a breaking change, since it's introducing quotes.
This will let steps like click on $element
to crash if more than one element was matched.
/* eslint-disable @typescript-eslint/no-explicit-any */
// @ts-ignore
import tableConverter from 'yadda/lib/converters/table-converter';
import { Server, ModelInstance, AnyAttrs, ModelInstanceShared } from 'ember-cli-mirage';
import { dasherize, camelize } from '@ember/string';
import { pluralize } from 'ember-inflector';
import { assert } from '@ember/debug';
// @ts-ignore
import HasMany from 'ember-cli-mirage/orm/associations/has-many';
// @ts-ignore
import BelongsTo from 'ember-cli-mirage/orm/associations/belongs-to';
import { Dict } from '@glimmer/interfaces';
const REGEX_COMMA_AND_SEPARATOR = /\s*,\s*|\s+and\s+/g;
const REGEX_REL_NAME = /(.+?)\((.+?)\)/;
const REGEX_ID_AND_TYPE = /@([^()]+)(?:\((.+?)\))?/;
function findRelationship(server: Server, type: string, relationshipName: string): HasMany | BelongsTo | undefined {
try {
const Model = server.schema.modelClassFor(type);
return Model.associationFor(relationshipName);
} catch (e) {} // eslint-disable-line no-empty
}
function findRelatedRecord(
server: Server,
idRaw: string,
relatedTypeFromRelationship: string
): ModelInstanceShared<AnyAttrs> {
const result = REGEX_ID_AND_TYPE.exec(idRaw);
if (!result || !result[1]) {
throw new Error(`Invalid id: ${idRaw}`);
}
const [, id, typeFromId] = result;
const relatedType = typeFromId || relatedTypeFromRelationship;
const relatedTypePlural = pluralize(camelize(relatedType));
const relatedCollection = server.schema[relatedTypePlural];
if (!relatedCollection) {
throw new Error(`Collection ${relatedTypePlural} does not exist in Mirage Schema`);
}
const relatedRecord = relatedCollection.find(id);
if (!relatedRecord) {
throw new Error(`Record of type ${relatedType} with id ${id} not found in Mirage Schema`);
}
return relatedRecord;
}
function findRelatedRecords(
server: Server,
type: string,
relationshipName: string,
idOrIdsRaw: string
): [ModelInstance | ModelInstance[] | null, string] {
idOrIdsRaw = idOrIdsRaw.trim();
let result;
let relationship;
let relatedType: string;
if (REGEX_REL_NAME.test(relationshipName)) {
const result = REGEX_REL_NAME.exec(relationshipName);
if (!result) {
throw new Error(`Regex parse error for realtionship name '${relationshipName}'`);
}
relationshipName = result[1];
relationship = findRelationship(server, type, relationshipName);
assert(`No such relationship "${relationshipName}" on Mirage model ${type}`, relationship);
relatedType = dasherize(result[2]);
} else {
relationship = findRelationship(server, type, relationshipName);
assert(`No such relationship "${relationshipName}" on Mirage model ${type}`, relationship);
relatedType = relationship.modelName;
}
// HasMany
if (relationship instanceof HasMany) {
result = idOrIdsRaw
.split(REGEX_COMMA_AND_SEPARATOR)
.filter((str) => str.length)
.map((idRaw: string) => findRelatedRecord(server, idRaw, relatedType));
// BelongsTo non-empty
} else if (idOrIdsRaw.length) {
result = findRelatedRecord(server, idOrIdsRaw, relatedType);
// BelongsTo empty
} else {
result = null;
}
return [result, relationshipName];
}
function seedFromRows(server: Server, typeRaw: string, rows: Array<any>): void {
const type = dasherize(typeRaw);
const typePlural = pluralize(camelize(typeRaw));
assert(`Collection ${typePlural} does not exist in Mirage`, !!server.db[typePlural]);
rows.forEach((row) => {
let traits: string[] = [];
const properties = Object.entries(row).reduce((result: Dict<any>, [key, value]: [string, any]) => {
key = key.trim();
value = value.trim();
// Relationship
if (REGEX_REL_NAME.test(key) || findRelationship(server, type, key)) {
[value, key] = findRelatedRecords(server, type, key, value);
// Traits
} else if (key === 'trait' || key === 'traits') {
traits = value.split(REGEX_COMMA_AND_SEPARATOR).filter((str: string) => str.length);
// Empty cell
} else if (value.length === 0) {
value = null;
// Numbers, Strings, Booleans, Arrays and Objects
} else {
try {
value = JSON.parse(value);
} catch (e) {
throw new Error(`Invalid JSON passed as "${key}"`);
}
}
result[key] = value;
return result;
}, {});
// @ts-ignore
delete properties.trait;
// @ts-ignore
delete properties.traits;
server.create(type, properties, ...traits);
});
}
export default function seedFromOpinionatedTable(server: Server, modelName: string, tableString: string): void {
let rows: Array<any>;
tableConverter(tableString.slice(1).trimRight(), (err: Error | null, result: Array<any>) => {
if (err) {
throw err;
}
rows = result;
});
// @ts-ignore
seedFromRows(server, modelName, rows);
}
Seeding steps are a mess. They are inconsistent and hard to grasp.
Also, this should be extracted into a converter:
const typePlural = pluralize(camelize(typeRaw));
assert(`Collection ${typePlural} does not exist in Mirage`, server.db[typePlural]);
We could wrap step invocation with a try-catch, where we re-throw any error with a more meaningful error message, by adding a step name and all arguments into the error message.
This will remove the necessity to add step name and error arguments in every step by hand.
Per this suggestion:
This is mostly unrelated to the specific changes here, but just stumbled upon the import path here. Given that the addon is exclusively meant to be used in tests, i.e. there is zero runtime code, I wonder if it wouldn't be nicer to omit the
test-support/
part here? Likeimport { givenSteps } from 'ember-cli-yadda-opinionated';
Technically this is possible, while still keeping all the imported code in the
test-support.js
bundle. See https://github.com/kaliber5/ember-window-mock/blob/master/index.js#L6-L23
powerSelectFindDropdown
fails when @searchEnabled
is set to true
on the powerselect.
It seems ember-power-select
removed the listBoxId
from aria-controls
when @searchEnabled
is set to true
with version 5.0.0
.
As a result the selector powerSelectDropdownIdForTrigger
cannot find the id and powerSelectFindDropdown
fails.
I guess I can build a workaround using the dropdown id that is still encoded in data-ebd-id
on the trigger.
Something like Menu-Item+Active
.
Currently ember-power-calendar is required as a dependency in the consuming app in order to run yadda-opinionated.
Caused by https://github.com/kaliber5/ember-cli-yadda-opinionated/blob/master/addon-test-support/index.js#L40
Picking this up from https://repository.m2p.net/mpp/mppfrontend/merge_requests/81#note_24511:
I still feel this could become a major PITA when this addon is used publicly, as it will provide ground for a lot of upgrade issues. Say we add a new step in a new version, that relies on a new converter. The step will probably added automatically to the app (see #10), but the user might forget adding the converter. And this will lead to weird errors (like the value pushed into the step is not converted, so just a string, and if you don't guard against that in every step using that converter, you will get type errors).
Or maybe you do some refactoring, and instead of doing some input conversion in the step, you have extracted that into a dedicated converter. When you can be sure that the new converter is available, you can make this a patch release, and the user won't even notice. If that's not the case, and the user is supposed to explicitly add the new converter, this would need a major version bump (as you cannot simply update the package without breaking things).
Beware of the cost of maintaining addons/libs, I know what I'm talking about! ๐
@lolmaus you certainly do as well, I just want to make sure we don't fall into a trap here!
We need both unit tests and acceptance tests.
Look for a way to write acceptance tests that expect a failure. Maybe it's possible to invoke steps programmatically?
Depends on #37
Picking this up from https://repository.m2p.net/mpp/mppfrontend/merge_requests/81#note_24518:
I split the steps in
ember-cli-yadda-opinionated
source because it's difficult to work with them when they all are in a single file. I'm even considering splitting files further as the library grows.
Agree, but that's the addon's internal structure, and here were are talking about its public API.
But as I said you can have both, and unless you export each individual step and just all given/when/then steps as a whole, I cannot see how the user is really able to pick and choose. I mean what use case could there be to pick all when steps (like without really knowing what steps they contain), but none of the then steps?
I would tend to allow composeSteps()
as well as something like addOptinionatedSteps()
, and eventually export each individual step maybe?
This addon is only offering utils: simple functions and objects. No Ember integration is being set up.
If we decouple the addon from Ember and convert it into a regular npm package, it will become available for all JS projects, including Ember thanks to ember-auto-import
.
One gotcha is that we'll be unable to use Ember's require
to conditionally import modules. But if we put code that uses such modules in separate files, it shouldn't be an issue. Don't import what you don't have dependencies for, and you're good to go.
As for Mocha vs QUnit, we might not need them at all! We could still offer two versions of composeSteps
, but in step implementations we could use Chai directly, regardless of the testing suite.
The QUnit version of composeSteps
would log every step, and in case of a failed assertion Chai would throw a normal error. The ergonomics will be slightly worse: QUnit will not be logging successful assertions, but who needs them? And the benefit is that we would have a single compact and efficient steps library, instead of two library copies, tightly-coupled to respective test suites.
We'll also avoid testing the QUnit version of the step library, which is much harder since we'll have to mock QUnit.
@simonihmig What do you think? :D
composeSteps
composeSteps
selectorFromLabel
selectorFromLabel
findByLabel
findByLabel
element
converterCurrently items from the labelMap are looked up through direct comparison. Would be nice to have support for both syntaxes. Possible implementation would be to always use one syntax for the lookup.
See:
Build fails due to an implicit dependency update incompatible with Node 8.
With a label like Some-Button of the second Child in the first Parent
, if the full selector is not found in DOM, you only get to see something like Expected a single element, but 0 found.
.
As we are actually splitting the path up, and iterating over all parts, it should be possible to give a better message like Expected a single element, but 0 found. No second Child found in first Parent
. Or based on selectors, like No [data-test-child].eq(2) found in [data-test-parent]:eq(1)
.
I would suggest to move all files (except for index.js
) in addon-test-support
to a -private
subfolder, to make sure nobody imports directly from them. Instead everything should be imported only from paths we explicitly declare as public API (by not having them in -private
). Only modules reexported in index.js
are public API.
This way we can freely refactor (e.g. create individual files for each step), without worrying about compatibility. I think this pattern is used frequently, and can be seen as best practice. E.g. https://github.com/orbitjs/ember-orbit/tree/master/addon
Non-technical staff may have hard time working with "objectIds": [{"id": "1", "type": "product-association"}]
format.
To make seeding steps more readable, we might want to provide a helper to define custom seeding steps. The helper would serve the following purposes:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.