adopted-ember-addons / ember-changeset

Ember.js flavored changesets, inspired by Ecto
http://bit.ly/ember-changeset-demo
MIT License
431 stars 141 forks source link

Don't work with ember 3.17 #436

Closed oliverlj closed 4 years ago

oliverlj commented 4 years ago

Hello guys,

I have this component :

export default class RegistrationForm extends Component<RegistrationFormArgs> {
  @service store!: StoreService;

  model = this.store.createRecord('user');
  changeset = new Changeset(this.model, lookupValidator(UserValidations), UserValidations);

My integrations test are failed with an update to ember 3.17 with this error :

not ok 46 Chrome 79.0 - [129 ms] - Integration | Component | registration-form: it show error on username already taken
     ---
         actual: >
             [object Object]
         stack: >
             Error: Assertion Failed: Underlying object for changeset is missing
                 at Object.assert (http://localhost:7357/assets/vendor.js:36647:15)
                 at changeset (http://localhost:7357/assets/vendor.js:136604:39)
                 at new ChangesetKlass (http://localhost:7357/assets/vendor.js:136656:13)
                 at changeset (http://localhost:7357/assets/vendor.js:136259:12)
                 at changeset (http://localhost:7357/assets/vendor.js:135532:37)
                 at http://localhost:7357/assets/vendor.js:6440:29
                 at deprecateMutationsInAutotrackingTransaction (http://localhost:7357/assets/vendor.js:56790:9)
                 at EmberHelperRootReference.fnWrapper [as fn] (http://localhost:7357/assets/vendor.js:6439:70)
                 at http://localhost:7357/assets/vendor.js:48147:38
                 at runInAutotrackingTransaction (http://localhost:7357/assets/vendor.js:56762:9)
         message: >
             Assertion Failed: Underlying object for changeset is missing
         negative: >
             false
         browser log: |
snewcomer commented 4 years ago

I am seeing the same thing in our tests. Looks like 3.17 was released yesterday. https://github.com/emberjs/ember.js/releases/tag/v3.17.0

In your case though, is the example wrong?

new Changeset(model vs. new Changeset(this.model?

oliverlj commented 4 years ago

all my app is in typescript, I don't think there is an impact with this keyword

snewcomer commented 4 years ago

Can you provide a bit more complete example? The PR description example seems like it is missing some pieces.

oliverlj commented 4 years ago

yes, sure, please find my component. //component utilisation

<RegistrationForm @onRegister={{action (perform register)}} />

//app/pods/components/registration-form/component.ts

import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import Component from '@glimmer/component';
import Changeset from 'ember-changeset';
import lookupValidator from 'ember-changeset-validations';
import { BufferedChangeset } from 'ember-changeset/types';
import StoreService from 'ember-data/store';
import UserValidations from './register-user-validations';

interface RegistrationFormArgs {
  onRegister: (changeset: BufferedChangeset) => void;
}

export default class RegistrationForm extends Component<RegistrationFormArgs> {
  @service store!: StoreService;

  model = this.store.createRecord('user');
  changeset: BufferedChangeset = Changeset(this.model, lookupValidator(UserValidations), UserValidations);

  get isRegisterButtonDisabled() {
    return (
      this.changeset.isInvalid ||
      !this.changeset.change.username ||
      !this.changeset.change.password ||
      !this.changeset.change.passwordConfirmation
    );
  }

  @action
  erasePasswordConfirmation() {
    if (this.changeset.change.passwordConfirmation) {
      this.changeset.set('passwordConfirmation', '');
    }
  }
}

//app/pods/components/registration-form/template.hbs

<form>
  <div class="input-group mb-3">
    <Input id="registration-form-input-username"
      class="form-control {{if changeset.error.username "is-invalid"}} {{if (and (not changeset.error.username) (not (is-empty changeset.change.username))) "is-valid"}}"
      placeholder="Nom d'utilisateur *" @value={{changeset.username}} />
    <div class="input-group-append">
      <div class="input-group-text">
        <FaIcon @icon="user" />
      </div>
    </div>
    {{#if changeset.error.username}}
      <div class="invalid-feedback">
        {{t (concat "register." changeset.error.username.validation ".username")}}
      </div>
    {{/if}}
  </div>
  <div class="input-group mb-3">
    <Input type="email" class="form-control {{if changeset.error.email "is-invalid"}}" placeholder="Email"
      @value={{changeset.email}} />
    <div class="input-group-append">
      <div class="input-group-text">
        <FaIcon @icon="envelope" />
      </div>
    </div>
    <div class="invalid-feedback">
      Veuillez saisir un email valide
    </div>
  </div>
  <div class="input-group mb-3">
    <Input type="password" id="registration-form-input-password"
      class="form-control {{if changeset.error.password "is-invalid"}} {{if (and (not changeset.error.password) (not (is-empty changeset.change.password))) "is-valid"}}"
      placeholder="Mot de passe *" @key-up={{action "erasePasswordConfirmation"}} @value={{changeset.password}} />
    <div class="input-group-append">
      <div class="input-group-text">
        <FaIcon @icon="lock" />
      </div>
    </div>
    <div class="invalid-feedback">
      Le mot de passe doit être au moins de 8 charactères
    </div>
  </div>
  <div class="input-group mb-3">
    <Input type="password" id="registration-form-input-passwordConfirmation"
      class="form-control {{if changeset.error.passwordConfirmation "is-invalid"}} {{if (and (not changeset.error.passwordConfirmation) (not (is-empty changeset.change.passwordConfirmation))) "is-valid"}}"
      placeholder="Retaper votre mot de passe *" @value={{changeset.passwordConfirmation}} />
    <div class=" input-group-append">
      <div class="input-group-text">
        <FaIcon @icon="lock" />
      </div>
    </div>
    <div class="invalid-feedback">
      Le mot de passe doit être identique.
    </div>
  </div>
  <div class="row">
    <div class="col-7">
      <div class="icheck-primary" data-toggle="tooltip" title={{t "application.not-yet-implemented"}}>
        <input type="checkbox" id="agreeTerms" name="terms" value="agree" disabled>
        <label for="agreeTerms">
          J'accepte
          <a class="btn-link disabled" href="#" aria-disabled="true">les conditions </a>
        </label>
      </div>
    </div>
    <div class="col-5">
      <button id="registration-form-submit" type="submit"
        class="btn btn-primary btn-block btn-flat {{if isRegisterButtonDisabled "disabled"}}"
        disabled={{this.isRegisterButtonDisabled}} {{action @onRegister changeset}}>
        S'inscrire
      </button>
    </div>
  </div>
</form>

//app/pods/components/registration-form/register-user-validations.ts

import { validateConfirmation, validateFormat } from 'ember-changeset-validations/validators';
import LoginUserValidations from 'fritzy-front/validations/login-user';

export const RegisterUserValidations = {
  email: validateFormat({ allowBlank: true, type: 'email' }),
  passwordConfirmation: validateConfirmation({ on: 'password' })
};

export default Object.assign({}, LoginUserValidations, RegisterUserValidations);

//tests/integration/pods/components/registration-form/component-test.ts

import { click, fillIn, render } from '@ember/test-helpers';
import { BufferedChangeset } from 'ember-changeset/types';
import { hbs } from 'ember-cli-htmlbars';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupIntl } from 'ember-intl/test-support';
import { setupRenderingTest } from 'ember-qunit';
import User from 'fritzy-front/models/user';
import { module, test } from 'qunit';
import sinon from 'sinon';
import { isChangeset } from 'validated-changeset';

module('Integration | Component | registration-form', hooks => {
  setupRenderingTest(hooks);
  setupIntl(hooks);
  setupMirage(hooks);

  test('it renders', async function(assert) {
    // Given
    this.set('registerAction', sinon.stub());

    // When
    await render(hbs`<RegistrationForm @onRegister={{action registerAction}} />`);

    // Then
    assert.dom('#registration-form-submit').exists('Registration form submit button not found!');
  });

  test('it register user', async function(assert) {
    // Given
    const registerAction = sinon.spy();
    this.set('registerAction', registerAction);
    await render(hbs`<RegistrationForm @onRegister={{action registerAction}} />`);

    await fillIn('#registration-form-input-username', 'oliver');
    await fillIn('#registration-form-input-password', 'password');
    await fillIn('#registration-form-input-passwordConfirmation', 'password');

    // When
    await click('#registration-form-submit');

    // Then
    assert.ok(registerAction.called, 'Register action shoud be called');
    assert.ok(isChangeset(registerAction.getCalls()[0].args[0]), 'A changeset should be pass at the register action');
  });

  test('it show error on username already taken', async function(assert) {
    // Given
    server.post(
      '/users',
      {
        errors: [
          {
            id: null,
            links: null,
            status: '422',
            code: null,
            title: 'key:error.back.user.not-unique-username',
            detail: null,
            source: { pointer: 'data/attributes/username', parameter: null },
            meta: null
          }
        ]
      },
      422
    );

    function registerAction(changeset: BufferedChangeset) {
      changeset.save().catch(() =>
        (changeset.data as User).get('errors').forEach(({ attribute, message }) => {
          changeset.pushErrors(attribute, message);
        })
      );
    }

    this.set('registerAction', registerAction);
    await render(hbs`<RegistrationForm @onRegister={{action registerAction}} />`);

    await fillIn('#registration-form-input-username', 'oliver');
    await fillIn('#registration-form-input-password', 'password');
    await fillIn('#registration-form-input-passwordConfirmation', 'password');

    // When
    await click('#registration-form-submit');

    // Then
    assert
      .dom('#registration-form-input-username.is-invalid')
      .exists('Registration form input username should be invalid');
  });
});
oliverlj commented 4 years ago

all test are in error, since the error happen on component initialization

snewcomer commented 4 years ago

What happens when you throw the following two lines in the constructor? Also I thought we need this.changeset in the template since it is a glimmer component.


constructor() {
  super(...arguments);
  model = this.store.createRecord('user');
  changeset: BufferedChangeset = Changeset(this.model, lookupValidator(UserValidations), UserValidations);
}
oliverlj commented 4 years ago

Without the new :

Uncaught TypeError: Class constructor ChangesetKlass cannot be invoked without 'new'
    at new RegistrationForm (component.ts:24)
    at EmberGlimmerComponentManager.createComponent (base-component-manager.js:39)
    at CustomComponentManager.create (index.js:5471)
    at Object.evaluate (runtime.js:3394)
    at AppendOpcodes.evaluate (runtime.js:2030)
    at LowLevelVM.evaluateSyscall (runtime.js:4932)
    at LowLevelVM.evaluateInner (runtime.js:4888)
    at LowLevelVM.evaluateOuter (runtime.js:4880)
    at JitVM.next (runtime.js:5823)
    at JitVM.execute (runtime.js:5807)
oliverlj commented 4 years ago

ok, so now with this.changeset in the template. it is working.

I have a Typescript error with new Changeset :

This expression is not constructable.
  Type 'typeof import("/home/oliver/git/fritzy-front/node_modules/ember-changeset/index")' has no construct signatures.ts(2351)
snewcomer commented 4 years ago

Perfect. And with the Changeset function you need to import it like { Changeset }.

Let me know if you have other issues!

oliverlj commented 4 years ago

Thanks for your time ! :) Maybe BufferedChangeset could be generic ? With this, this.changeset.data will return the good type