Application Developer Festival 2015 バグフィックスチャレンジ フロントエンド問題 解答・解説

この記事は Application Developer Festival 2015 内で行われたバグフィックスチャレンジのフロントエンド問題の解答・解説記事です. この問題は私が作成し, 採点を行ったものです.

問題

バグフィックスチャレンジ全体のルールはこちらに掲載しています. フロントエンド問題はこちらで公開しています.

フロントエンド問題は HTML, CSS, JavaScript それぞれ1つのファイルからなるシンプルなアプリケーションに複数のバグが仕込まれており, バグを修正して仕様どおりに動作するようにするというものです.

この問題のポイントの一つとしては最新の Google Chrome の安定版 (当時 41.0.2272.101) で動作すればよいとされているため, HTML5 や CSS3, ECMAScript 6 のような最新の技術を用いて作成されているということがあります.

もう一つはこの問題がバグフィックスチャレンジであり, ソースコードを全面的に書き換えて仕様通りに動くようにすればいいというのではなく, 既存のコード中のバグを修正してもらう問題だということです. 実際にある行に含まれているバグは, その行だけを修正すれば良いように問題が作成されています.

解答・解説

HTML

文字コードのバグについては多くの方が修正していましたが, input 要素のバグは修正できている方は多くありませんでした.

文字コード

<meta charset="Shift_JIS">

index.html は UTF-8 でエンコードされていますが, HTML コード中で文字コードが Shift_JIS であると宣言されています. ファイルを Shift_JIS で保存し直すか, 文字コード宣言を UTF-8 に修正することで対応できます.

<meta charset="UTF-8">

input 要素

<li id="add-user-row"><input id="add-user-input" type="text"></input><a id="add-user-button">+</a></li>

input 要素は空要素 (void elements) です. HTML5 では空要素に終了タグを記述することとは許されていません.

Void elements only have a start tag; end tags must not be specified for void elements.
8 The HTML syntax — HTML5

終了タグを削除することで修正出来ます.

<li id="add-user-row"><input id="add-user-input" type="text"><a id="add-user-button">+</a></li>

CSS

2つともに正解者は少なかったです. 文法の問題については正解者が1人しかいませんでした.

color

background-color: hsl(263, "95%", "34%");

CSS3 では HSL (色相・彩度・明度) を使った色指定が出来るようになりました. 彩度と明度は percentage として指定します.

HSL colors are encoding as a triple (hue, saturation, lightness). (中略) Saturation and lightness are represented as percentages.
CSS Color Module Level 3

percentage は <number>% の形式で表されます.

A percentage value is denoted by , consists of a immediately followed by a percent sign ‘%’.
CSS Values and Units Module Level 3

不要な引用符を削除することで修正できます.

background-color: hsl(263, 95%, 34%);

文法

border: 0

CSS ではそれぞれの宣言の間をセミコロンで区切る必要があります.

Each declaration has a name, followed by a colon and the declaration value. Declarations are separated by semicolons.
CSS Syntax Module Level 3

セミコロンを追加することで修正できます.

border: 0;

JavaScript

JavaScript のコードに多くのバグを仕込んでいました. DOM 操作については比較的多くの人が正解できていましたが, ECMAScript 6 の最新仕様の部分や, 乱数生成の部分はあまり正解できていませんでした.

HTMLCollection

// 1つ目
if (0 < document.getElementsByClassName("send-hi-button").filter(
      function(button) { return username === button.dataset.username; }).length) {
  return;
}

// 2つ目
var buttons = document.getElementsByClassName("send-hi-button");
buttons.forEach(function(button, i) {
});

document.getElementsByClassName は HTMLCollection を返します.

The getElementsByClassName(classNames) method must return the list of elements with class names classNames for the context object.
DOM Standard

The list of elements with class names classNames for a node root is the HTMLCollection returned by the following algorithm:
DOM Standard

HTMLCollection は Array の実装している filterforEach を実装していません.

修正方法の例として Array.prototype.filter.call のようにして, Arrayの実装を借用する方法があります. 他にも [].filter とする方法や, for 文を使って書き換える方法が考えられます.

// 1つ目
if (0 < Array.prototype.filter.call(
      document.getElementsByClassName("send-hi-button"),
      function(button) { return username === button.dataset.username; }).length) {
  return;
}

// 2つ目
var buttons = document.getElementsByClassName("send-hi-button");
Array.prototype.forEach.call(buttons, function(button, i) {
});

Math.random

var hue = Math.round(Math.random() * 359);

仕様より色相は0度〜359度の360からランダムに選択する必要がある. Math.random は0以上, 1未満の Number を返す.

Returns a Number value with positive sign, greater than or equal to 0 but less than 1, chosen randomly or pseudo randomly with approximately uniform distribution over that range, using an implementation-dependent algorithm or strategy. This function takes no arguments.
ECMAScript Language Specification – ECMA-262 Edition 5.1

Math.round は最も近い整数値に値を丸めます. (四捨五入)

Returns the Number value that is closest to x and is equal to a mathematical integer.
ECMAScript Language Specification – ECMA-262 Edition 5.1

バグのあるコードでは 0 と 359 になる確率が他の値になる確率の半分になってしまいます.

修正方法の1つとして 359 を 360 に変更する方法があります. この場合 hue が 360 になってしましますが. 360 は 0 として扱われるため問題ありません.

By definition red=0=360, and the other colors are spread around the circle, so green=120, blue=240, etc. As an angle, it implicitly wraps around such that -120=240 and 480=120. One way an implementation could normalize such an angle x to the range [0,360) (i.e. zero degrees, inclusive, to 360 degrees, exclusive) is to compute (((x mod 360) + 360) mod 360).
CSS Color Module Level 3

var hue = Math.round(Math.random() * 360);

もう一つの方法として 359 を 360 に変更して, Math.roundMath.floor にする方法があります. これは与えられた値を超えない最大の数を返す (切り捨て) ので, 仕様どおりに動作します.

Returns the greatest (closest to +∞) Number value that is not greater than x and is equal to a mathematical integer.
ECMAScript Language Specification – ECMA-262 Edition 5.1

var hue = Math.floor(Math.random() * 360);

Template literals

button.style.backgroundColor = 'hsl(${hue}, ${SATURATION}, ${LUMINANCE})';

ECMAScript 6 の Draft に Template literals が入っています. Template literals はバッククオート ` で囲んで表記するため, 以下のように修正すると動作します.

button.style.backgroundColor = `hsl(${hue}, ${SATURATION}, ${LUMINANCE})`;

innerHTML

button.innerHTML = button.dataset.username = username;
selectedButton.innerHTML = "Send Hi!";
selectedButton.innerHTML = selectedButton.dataset.username;

innerHTML を利用すると script 要素などの任意の HTML を埋め込まれる恐れがあります. これは必ずしもバグではありませんが改善していた方には得点を与えました.

単純な改善方法は, innerHTMLtextContent に書き換える方法です. Google Chrome ではこれによりエスケープされて表示されます.

button.textContent = button.dataset.username = username;
selectedButton.textContent = "Send Hi!";
selectedButton.textContent = selectedButton.dataset.username;

他の修正方法として入力された文字列をエスケープする方法を考えられます. このようなエスケープ処理は Underscore.js 等にも実装されています.

insertBefore

addUserRow.insertBefore(button, addUserRow);

insertBefore挿入するノードの親ノード.insertBefore(挿入するノード, 挿入するノードの直前のノード) のように使います.

Inserts the node newChild before the existing child node refChild. If refChild is null, insert newChild at the end of the list of children.
If newChild is a DocumentFragment object, all of its children are inserted, in the same order, before refChild. If the newChild is already in the tree, it is first removed.
Document Object Model Core

修正例は以下のようになります.

addUserRow.parentNode.insertBefore(button, addUserRow);

変数宣言

変数 flag が宣言されずに利用されています.

index.js は strict モードで動作するように記述されていますが, strict モードでは宣言されていない変数に値を代入しようとした場合, エラーを投げます.

Assignment to an undeclared identifier or otherwise unresolvable reference does not create a property in the global object.
ECMAScript Language Specification – ECMA-262 Edition 5.1

修正例は以下のようになります.

var flag = false;

ロジック

sortedButtons[flag ? i + 1 : i] = button;

これは単純にロジックの実装誤りです. ii + 1 を置き換えると修正できます.

sortedButtons[flag ? i : i + 1] = button;

for

for (let button in sortedButtons) {}

for...in はオブジェクトのプロパティ名 (キー) について反復するのに対して, for...of はオブジェクトのプロパティ (値) について反復します. この場合は後者の動作が望まれるため以下のように修正します.

for (let button of sortedButtons) {}

この処理は for 文などでも書き換えられますが, たった2文字だけ変更して動作する上の修正方法が良いと思います.

まとめ

ここまでの回答をまとめたものをこちらで公開しています. また, 想定外のバグとして “Sent” を “Send” と typo しているというものがありました.

今回の問題では残念ながら全問正解の方はいませんでした. この問題が基本的な部分と最新の仕様の両方について改めて復習するきっかけになれば幸いです.