tag:crieit.net,2005:https://crieit.net/tags/dom/feed 「dom」の記事 - Crieit Crieitでタグ「dom」に投稿された最近の記事 2022-04-23T01:06:29+09:00 https://crieit.net/tags/dom/feed tag:crieit.net,2005:PublicArticle/18173 2022-04-23T01:06:29+09:00 2022-04-23T01:06:29+09:00 https://crieit.net/posts/jest-operate-dom-and-ignore-console-error-20220425 Jest で DOM 操作の試験、 console.error を無視 <p>Jest によるテストコードを書く上でいくつか嵌まったポイントをピックアップ。</p> <h2 id="DOM 操作の試験"><a href="#DOM+%E6%93%8D%E4%BD%9C%E3%81%AE%E8%A9%A6%E9%A8%93">DOM 操作の試験</a></h2> <p>まず引っかかったのは DOM 操作。今回はバリバリ DOM を操作したりある要素が存在しているかのチェックをする、といった完全に DOM ありきのコードを対象にしていました。</p> <p>機能のユニットテストであれば直感的に分かるのですが、 DOM 操作って……。</p> <h3 id="sample/assert.js"><a href="#sample%2Fassert.js">sample/assert.js</a></h3> <pre><code class="js">const dom = ` <div id="hoge"></div> `; module.exports = dom; </code></pre> <h3 id="sample/exception.js"><a href="#sample%2Fexception.js">sample/exception.js</a></h3> <pre><code class="js">const dom = ` <!--<div id="hoge"></div>--> `; module.exports = dom; </code></pre> <h3><strong>tests</strong>/hoge.test.js</h3> <pre><code class="js">const hoge = require('../src/hoge'); const domAssert = require('../sample/assert'); const domException = require('../sample/exception'); test('method:hoge test:assert', () => { document.body.innerHTML = domAssert; const hogeDOM = document.querySelector('#hoge'); const hogeInstance = new hoge(); expect(hogeInstance.judge(hogeDOM)).toBe(true); }); test('method:hoge test:exception', () => { document.body.innerHTML = domException; const hogeDOM = document.querySelector('#hoge'); const hogeInstance = new hoge(); expect(hogeInstance.judge(hogeDOM)).toBe(false); }); </code></pre> <p>例えば <code>hoge</code> という <code>id</code>属性 が付与された DOM が存在するか否かのチェックをする <code>hoge</code>クラス に対して <code>judge()</code>メソッド をテストしたい場合。</p> <ol> <li>DOM となるHTMLタグのコードを JS の文字列としてセット</li> <li>それを <code>module.exports</code></li> <li>テストコードで <code>require()</code> し、コード内の <code>document.body.innerHTML</code> へ代入する</li> <li>後は通常の JS のように <code>document.querySelector</code> なり何なりで操作やチェックが可能。属性であれば <code>getAttribute()</code> してあげれば良い</li> </ol> <p>……わりと素直にできることが分かりました。 Jest すごい。</p> <h3 id="The error below may be caused by using the wrong test environment"><a href="#The+error+below+may+be+caused+by+using+the+wrong+test+environment">The error below may be caused by using the wrong test environment</a></h3> <p>さて、上述でわりと直感的に DOM 操作した上でテストコードを記述できる、と記載しましたがいざテストをしようとすると以下のエラーが発生しました (Jest が 27系 以上)。</p> <blockquote> <p>The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string.<br /> Consider using the "jsdom" test environment.</p> </blockquote> <p>これは Jest に DOM 操作用の設定が必要なためのようです。</p> <p>解決策としては2つ。</p> <h4 id="テストコード内に設定を記述"><a href="#%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89%E5%86%85%E3%81%AB%E8%A8%AD%E5%AE%9A%E3%82%92%E8%A8%98%E8%BF%B0">テストコード内に設定を記述</a></h4> <p>まずはテストコード内に設定を記述する方法。これはテストコードごとに設定を変更できる柔軟性がある一方、必要な場合は全てのテストコードで同じ記述をする必要があるため数が多いと面倒になる、というパターン。</p> <pre><code class="js">/** * @jest-environment jsdom */ </code></pre> <h4 id="jest.config.js"><a href="#jest.config.js">jest.config.js</a></h4> <p>もう1つは <code>jest.config.js</code> に記述する方法。こちらは一括指定ができるものの、テストコードごとの設定はできないので先程とは逆ですね。</p> <pre><code class="js">module.exports = { testEnvironment: 'jsdom', // 追加 coverageDirectory: 'coverage' }; </code></pre> <h2 id="console.error() を無視"><a href="#console.error%28%29+%E3%82%92%E7%84%A1%E8%A6%96">console.error() を無視</a></h2> <p>テスト対象のコードで例外等の際に <code>console.error()</code> を使用していると、そのコードに入ったときに Jest が止まってしまいます。</p> <p>そこで、 <code>console.error()</code> に遭遇しても Jest が止まらないようにする方法を。</p> <pre><code class="js">const hoge = require('../src/hoge'); const domException = require('../sample/exception'); test('method:fuga test:exception', () => { jest.spyOn(console, 'error').mockImplementation((mes) => { console.log(mes); }); document.body.innerHTML = domException; const hogeDOM = document.querySelector('#hoge'); const hogeInstance = new hoge(); expect(hogeInstance.fuga(hogeDOM)).toBe(false); }); </code></pre> <p>例えばこんな感じ。 <code>jest.spyOn(console, 'error') ...</code> を実際のコードを走らせる前に記述しておくことで <code>console.error()</code> で止まらなくなります。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h3 id="DOM操作"><a href="#DOM%E6%93%8D%E4%BD%9C">DOM操作</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://crudzoo.com/blog/jest">JestでjQuery主体のJavaScriptをテストする① | Crudzoo</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://jestjs.io/ja/docs/tutorial-jquery">DOM 操作 · Jest</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://tech.excite.co.jp/entry/2021/12/10/100000">DOM操作をするjQueryコードをJestでテストする方法を勉強した話 - エキサイト TechBlog.</a></li> </ul> <h3 id="The error below may be caused by using the wrong test environment"><a href="#The+error+below+may+be+caused+by+using+the+wrong+test+environment">The error below may be caused by using the wrong test environment</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/mame_daifuku/items/79b6a5a1514a3f067e1a">Jestの「 The error below may be caused by using the wrong test environment」の解決方法 - Qiita</a></li> </ul> <h3 id="undefined 判定"><a href="#undefined+%E5%88%A4%E5%AE%9A">undefined 判定</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://jestjs.io/ja/docs/expect#tobeundefined">Expect · Jest</a></li> </ul> <h3 id="null 判定"><a href="#null+%E5%88%A4%E5%AE%9A">null 判定</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://jestjs.io/ja/docs/expect#tobenull">Expect · Jest</a></li> </ul> <h3 id="(未使用) 非同期"><a href="#%28%E6%9C%AA%E4%BD%BF%E7%94%A8%29+%E9%9D%9E%E5%90%8C%E6%9C%9F">(未使用) 非同期</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://jestjs.io/ja/docs/asynchronous">非同期コードのテスト · Jest</a></li> </ul> <h3 id="console.errorで止まらないように"><a href="#console.error%E3%81%A7%E6%AD%A2%E3%81%BE%E3%82%89%E3%81%AA%E3%81%84%E3%82%88%E3%81%86%E3%81%AB">console.errorで止まらないように</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/nus3/scraps/f1ea3cb4982593">Jest 逆引き テストケース</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://blog.kozakana.net/2021/02/dont-stop-with-console-error-when-running-tests-in-jest/">jestでテスト実行時console.errorで止まらないようにする | Simple is Beautiful.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/manten120/items/790de8e1612fc8a15355">【Jest】console.log()をmock化する方法2通り - Qiita</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/16541 2021-01-06T02:05:48+09:00 2021-01-06T02:05:48+09:00 https://crieit.net/posts/EventTarget EventTargetとは <p>EventTargetはイベントのターゲットとなるオブジェクトです。<br /> イベントリスナーの登録や削除を行えます。</p> <h2 id="EventTargetのメソッド"><a href="#EventTarget%E3%81%AE%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89">EventTargetのメソッド</a></h2> <h3 id="addEventListener - イベントリスナの登録"><a href="#addEventListener+-+%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%AA%E3%82%B9%E3%83%8A%E3%81%AE%E7%99%BB%E9%8C%B2">addEventListener - イベントリスナの登録</a></h3> <p>addEventListenerの第1引数にイベントの名称、<br /> 第2引数にイベント発生時に実行する関数を指定する事でイベントの登録が行なえます。</p> <p>例えば、クリックのイベントは以下のように登録できます。<br /> buttonAをクリックするとdiv要素のクリックイベントも呼び出されます。</p> <pre><code class="html"><div id="outer"> <div id="inner"> <button id="buttonA">A</button> </div> </div> </code></pre> <pre><code class="javascript">document.getElementById('outer').addEventListener('click',function(){console.log('outer');}); document.getElementById('inner').addEventListener('click',function(){console.log('inner');}); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA');}); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA2');}); //buttonAをクリックする => buttonA buttonA2 inner outer </code></pre> <p>第3引数としてbooleanを指定可能で、上記はfalseを指定した以下と同様です。</p> <pre><code class="javascript">document.getElementById('outer').addEventListener('click',function(){console.log('outer');},false); document.getElementById('inner').addEventListener('click',function(){console.log('inner');},false); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA');},false); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA2');},false); //buttonAをクリックする => buttonA buttonA2 inner outer </code></pre> <p>第3引数としてtrueを指定すると、実行の順序が逆になります。</p> <pre><code class="javascript">document.getElementById('outer').addEventListener('click',function(){console.log('outer');},true); document.getElementById('inner').addEventListener('click',function(){console.log('inner');},true); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA');},true); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA2');},true); //buttonAをクリックする => outer inner buttonA buttonA2 </code></pre> <p>イベントリスナーが登録されていると、イベント発生元となった要素を含む親の要素でもそのイベントが実行されます。<br /> そのイベントの実行順序が3つ目の引数で制御できるようになっています。</p> <p>イベントの伝播はwindowオブジェクトからイベント発生元まで下位へ辿っていくキャプチャーフェーズと、<br /> イベント発生元から上位要素へと辿っていくバブリングフェーズがあります。<br /> つまり、「ouuter -> inner -> buttonA -> inner -> outer」の順序でイベントが伝播し、<br /> 前半がキャプチャーフェーズ、後半がバブリングフェーズとなっています。<br /> 引数をtrueとするとキャプチャーフェーズでイベントが実行されるため順序が逆となったというわけです。</p> <p>もちろん混在も可能で、次のようにすると「outer -> buttonA -> inner」と実行がされます。</p> <pre><code class="javascript">document.getElementById('outer').addEventListener('click',function(){console.log('outer');},true); document.getElementById('inner').addEventListener('click',function(){console.log('inner');},false); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA');}); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA2');}); //buttonAをクリックする => outer buttonA buttonA2 inner </code></pre> <p>イベントの伝播はstopPropagation()で止める事ができます。</p> <pre><code class="javascript">document.getElementById('outer').addEventListener('click',function(){console.log('outer');}); document.getElementById('inner').addEventListener('click',function(e){console.log('inner');e.stopPropagation();}); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA');}); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA2');}); //buttonAをクリックする => buttonA buttonA2 inner </code></pre> <p>ただし、stopPropagation()だと同じ要素に登録したリスナーの実行は止められません。</p> <pre><code class="javascript">document.getElementById('outer').addEventListener('click',function(){console.log('outer');}); document.getElementById('inner').addEventListener('click',function(){console.log('inner');}); document.getElementById('buttonA').addEventListener('click',function(e){console.log('buttonA');e.stopPropagation();}); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA2');}); //buttonAをクリックする => buttonA buttonA2 </code></pre> <p>同じ要素のリスナーも含め、イベントの実行を止めるには、stopImmediatePropagation()を使います。</p> <pre><code class="javascript">document.getElementById('outer').addEventListener('click',function(){console.log('outer');}); document.getElementById('inner').addEventListener('click',function(){console.log('inner');}); document.getElementById('buttonA').addEventListener('click',function(e){console.log('buttonA');e.stopPropagation();}); document.getElementById('buttonA').addEventListener('click',function(){console.log('buttonA2');}); //buttonAをクリックする => buttonA </code></pre> <p>既定の動作、例えばチェックボックスクリック時のチェックを止めたい場合はpreventDefault()で処理を停止できます。</p> <pre><code class="html"><label><input id="check" type="checkbox" />Check</label> </code></pre> <pre><code class="javascript">document.getElementById('check').addEventListener('click',function(e){e.preventDefault();}); </code></pre> <p>addEventListenerの第3引数には、より複雑なオブジェクトをオプションとして指定する事もできます。</p> <pre><code class="javascript">document.getElementById('outer').addEventListener('click',function(e){console.log('outer');e.preventDefault();},{ capture : true , once : true, passive :false, }); document.getElementById('inner').addEventListener('click',function(e){console.log('inner');e.preventDefault();},{ capture : true , once : true, passive :true, }); document.getElementById('buttonA').addEventListener('click',function(e){console.log('buttonA');e.preventDefault();},{ capture : true , once : false, passive :false, }); //buttonAをクリックする(1回目) => outer inner (警告) buttonA //buttonAをクリックする(2回目) => buttonA </code></pre> <p>「capture」は第3引数にbooleanを指定したときと同じものです。trueの場合にバブリングフェーズでのイベント実行が行われます。<br /> 「once」はtrueの場合、イベントの実行を1回行うとリスナは削除されます。<br /> 「passive」はtrueの場合、preventDefault()がリスナで呼ばれるとコンソールに警告が出力されます。</p> <h3 id="removeEventListener - イベントリスナの削除"><a href="#removeEventListener+-+%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%AA%E3%82%B9%E3%83%8A%E3%81%AE%E5%89%8A%E9%99%A4">removeEventListener - イベントリスナの削除</a></h3> <p>removeEventListenerを使うと、指定したイベントからリスナを削除できます。</p> <pre><code class="html"><div id="outer"> <div id="inner"> <button id="buttonA">A</button> </div> </div> </code></pre> <pre><code class="javascript">const clickListener1 = function(){console.log('buttonA1');}; const clickListener2 = function(){console.log('buttonA2');}; document.getElementById('buttonA').addEventListener('click',clickListener1); document.getElementById('buttonA').addEventListener('click',clickListener2); document.getElementById('buttonA').removeEventListener('click',clickListener1); //buttonAをクリックする => buttonA2 </code></pre> <p>第2引数を指定すると、キャプチャーフェーズで実行されるリスナであるかが一致する場合にだけ削除が行われます。</p> <pre><code class="javascript">const clickListener1 = function(){console.log('buttonA1');}; const clickListener2 = function(){console.log('buttonA2');}; document.getElementById('buttonA').addEventListener('click',clickListener1,false); document.getElementById('buttonA').addEventListener('click',clickListener2,true); document.getElementById('buttonA').removeEventListener('click',clickListener1,false); document.getElementById('buttonA').removeEventListener('click',clickListener2,false); //buttonAをクリックする => buttonA2 </code></pre> portaloo