Giter VIP home page Giter VIP logo

ember-cli-yadda-opinionated's Introduction

ember-cli-yadda-opinionated

Travis ember-cli

Development

Use Yarn.

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.

ember-cli-yadda-opinionated's People

Contributors

dagroe avatar ember-tomster avatar lolmaus avatar simonihmig avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

Forkers

dagroe

ember-cli-yadda-opinionated's Issues

Split compound selectors safely

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?

Use custom converters instead of capturing groups everywhere

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.

Include the seeding helper

/* 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);
}

Refactor seeding steps

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]);

Wrap each step with try/catch for centralized error message

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.

Shorten import paths

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? Like

import { 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

Cannot find dropdown for trigger when @searchEnabled is true

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.

Refine public API for adding addon provided converters

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!

Cover steps with tests

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?

Refine public API for adding addon provided steps

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?

Decouple from Ember

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

Implement basic functionality

  • Implement composeSteps
  • Test composeSteps
  • Implement selectorFromLabel
  • Test selectorFromLabel
  • Implement findByLabel
  • Test findByLabel
  • Implement the element converter
  • Implement label mapping
  • Test label mapping
  • Try it in a real app
  • Explain the vision in the readme

Update dependencies

Build fails due to an implicit dependency update incompatible with Node 8.

Better debugability for nested selectors

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).

Restructure public exports

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

Provide a helper to define custom seeding steps for readability

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:

  • Map attribute names to more human-readable and feature-focused equivalents.
  • Define the type of each attribute/relationship for:
    • Runtime type checking.
    • Avoid the visual noise of quotes, curlies and square brackets, which were previously necessary for guessing types.
  • Provide a simple, readable way to specify polymorphic relationships.
  • Offer traits โ€” both Mirage-based and inline.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.