今日の一枚 image

prev 2021-07-13Movidea開発日記 Cypress、containsはDOMが選択されるが型がundefined ts

cy.get("div[data-testid='2']").should((x) => {}); // x is JQuery<HTMLElement>
cy.contains("A B").should((x) => {}); // x is undefined

これはDOMを取得する方法としての実装とアサーションとしての実装の両方があるせい。 こんな感じになってるが、cyがChainable<undefined>だからSubject=undefinedになってしまう。設計が悪い、名前を分けるべき。 cypress.d.ts

contains(content: string | number | RegExp, options?: Partial<Loggable & Timeoutable & CaseMatchable & Shadow>): Chainable<Subject>
contains<E extends Node = HTMLElement>(content: string | number | RegExp): Chainable<JQuery<E>>
...
interface cy extends Chainable<undefined> {}

それはさておき自分の実装としてはdata-testidを付けてあるので、それを使って選択するのをカスタムコマンドにしてやりやすくしようと考えた。

support/index.ts

declare global {
  namespace Cypress {
    interface Chainable {
      ...
      testid(testid: string): Chainable<Element>;
      hasPosition(x: number, y: number): Chainable<Element>;
    }
  }
}
 
Cypress.Commands.add("testid", (testid: string) => {
  return cy.get(`*[data-testid='${testid}']`);
});
 
Cypress.Commands.add(
  "hasPosition",
  {
    prevSubject: true,
  },
  (subject: Cypress.Chainable<Element>, x: number, y: number) => {
    const cr = subject[0].getBoundingClientRect();
    expect(cr.x).equal(x);
    expect(cr.y).equal(y);
    return subject;
  }
);

これで cy.testid("1").hasPosition(159, 174); と書けるようになった。 しかしこれはリトライが行われない。

実際のところCustom Commands | Cypress Documentationには子コマンドの作り方は書いてあるが例ではアサーションをしていない。

Assertions | Cypress Documentationを見るとChaiにアサーションを追加しろと書いてある。 Adding Chai Assertionsを参考に書いてみた。 support/index.ts

chai.use((_chai, utils) => {
  function hasPosition(options) {
    const [x, y] = options;
    const cr = this._obj[0].getBoundingClientRect();
 
    this.assert(
      cr.x === x,
      `expected x:${cr.x} is ${x}`,
      `expected x:${cr.x} is not ${x}`,
      cr.x
    );
    this.assert(
      cr.y === y,
      `expected y:${cr.y} is ${y}`,
      `expected y:${cr.y} is not ${y}`,
      cr.y
    );
  }
 
  _chai.Assertion.addMethod("hasPosition", hasPosition);
});

これで cy.testid("1").should("hasPosition", [159, 174]); と書けるようになった。 shouldだったらhaveだなと思うがそういうことを言い出すとそもそもChai的には cy.testid("1").has.position([159, 174]); と書けるべきなのでは、みたいな気持ちになってきて、これはCypressとChaiの境目あたりにあって話がややこしいので保留した。

下記のテストが簡潔に書けるようになった test.ts

// before
cy.get("div[data-testid='1']").should((x) => {
  expect(x[0].getBoundingClientRect().x).equal(x1);
  expect(x[0].getBoundingClientRect().y).equal(y1);
});
// after
cy.testid("1").should("hasPosition", [x1, y1]);

だいぶカスタムコマンドに慣れてきて、そもそもupdateGlobalもよく使うからカスタムコマンドにしたらいいなと気づいた support/index.ts

Cypress.Commands.add("updateGlobal", (callback: (g: State) => void) => {
  return cy.movidea((movidea) => {
    movidea.updateGlobal(callback);
  });
});

下記のようにいちいちmovideaを介さなくても状態更新ができるようになった test.ts

// before
cy.movidea((movidea) => {
  movidea.updateGlobal((g) => {
    g.itemStore["1"].position = [dx, 0];
  });
});
// after
cy.updateGlobal((g) => {
  g.itemStore["1"].position = [dx, 0];
});

「アイテム3番(付箋B)を動かしたらアイテム1番(グループ)の位置はどこどこになるべき」というテスト。(アイテムIDもわかりやすい名前にした方がよかったな) test.ts

cy.updateGlobal((g) => {
  g.itemStore["3"].position = [-100, 0];
});
cy.testid("1").should("hasPosition", [55, 170]);

image

で、この状態からグループを閉じると場所がずれる。次はこれを直していこう。 test.ts

cy.updateGlobal((g) => {
  (g.itemStore["1"] as GroupItem).isOpen = false;
});

image

Cypress、テストの各段階のスナップショットが見れるので「微妙に4ピクセルずれるバグ」を確認することができた、便利 image

ここいらでもう一度RegroupからエクスポートされたJSONを表示してみよう。なるほどここまでちゃんとなると何がバグの原因かわかりやすい、グループの中のグループのバウンディングボックスが平行移動の情報を失ってるのが原因だな。 image

直した。色々なところに波及するが、テストケースがあるので動作確認しやすくて良い。ちゃんとした見栄えになった。 image Regroupでの見え方 from 60df19d4aff09e0000c6d717 image ほぼ同じ! 今までのテストケースも全部成功する。

明日はメニューやダイアログ周りをやる https://material-ui.com/components/menus/ OOUIを読み返した OOUIの解説を読み直してたんだけど「新規作成」も「インポート」もタスク指向なんだよなぁ オブジェクト指向UIにするとしたらどうなるのだろう 既存のマップを開くURLではないものにアクセスする時点で「新規作成」なのは当たり前だし、新規作成した後コンテンツを入れないということも考えにくいのだからそもそも最初からインポートウィザードが開いているべきなのかな

インポートではなく「付箋の追加」ではないか

  • URLからインポートするときのUIは?

Next 2021-07-15Movidea開発日記