playwrightでElementのattributesを判定してなにかをするには

最近は狂気のe2eしている。狂気のe2eの一例としては自分のサイトではないDOMの要素に対してテストするといった狂気的行動がある。

自分の作っているサイトなりアプリケーションならソースコードに手を入れてdata-testidを付与することでe2eのシナリオを比較的簡単に書けるが、自分のサイトでないならそんな都合のいいdata属性はないため、狂気のDOM芸を繰り出すほかない。

それで、シナリオ内でattributesを取得してhogeしたい

たとえば、ある要素のattributesにAという属性があったら、Xボタンをクリックして、B属性があったらYボタンをクリックして、C属性があったらZボタンを押すみたいなシナリオを想定する。

playwrightには toHaveAttribute というアサーションはあるが、expectと合わせて使う想定でPromise<void>なのでこういう判定には基本使いにくい…。

普通にElement.attributesを参照してワイワイできればよいのだけど

Element.attributesNamedNodeMap型だが、これを素直に取り出すことはできない。なぜなら、playwrightのevaluateはV8環境のサンドボックス内で行われるため素直に持ちだしたものをnode環境のように取り回すことができない。

Evaluating JavaScript | Playwright

なので、NamedNodeMapを変換するモジュールを書く。

export type NamedNodeMapLike = Record<string, string>;

export function convertToNamedNodeMapLike(
   attributes: NamedNodeMap,
): NamedNodeMapLike {
    return Array.from(attributes).reduce<NamedNodeMapLike>((acc, attr) => {
        acc[attr.name] = attr.value;
        return acc;
    }, {});
}

どうするか

Playwrightのシナリオで以下のようにする。

const element = await page.locator("hoge")
const attrs = await element.evaluate(
  (el, convertToNamedNodeMapLike) => {
    return Function(`return ${convertToNamedNodeMapLike}`)()(
    el.attributes,
    );
  },
  convertToNamedNodeMapLike.toString(),
);

かなりハック技をしている。まずevaluate関数(V8サンドボックスで実行される)にモジュールを渡すにはシリアライズする必要があるため convertToNamedNodeMapLike.toString() する。そのうえで引数としてElement.attributesを渡して実行してあげる必要があるので、evalないしはFunctionで評価してあげないといけない。

こうすることで、例えばattributesの内容によって判定するロジックをNamedNodeMap を受け取ってなにかする関数へ切り出しておきユニットテストが書けるようになる。e2e以外の場所で同じロジックを再利用することもできる。


狂気のe2eを成し遂げるには狂気のevaluateをしないといけない。こんなことしなくてもPlaywrightがシャキッとなんかやってくれたらよかったのだけど、そんな世界にはなっていない。