168 lines
7.6 KiB
TypeScript
168 lines
7.6 KiB
TypeScript
// @vitest-environment jsdom
|
||
import { describe, it, expect } from 'vitest';
|
||
import { buildDumpHtmlScript } from './browser.js';
|
||
|
||
/**
|
||
* dumpHtml がページ内で走らせる文字列を jsdom の上で実際に評価して、
|
||
* 生の HTML に入力欄の中身が載らないことを見る(issue #1236 レビュー)。
|
||
*
|
||
* 入力した文字列を `value` **属性**へ書き戻すページ(React の制御された
|
||
* input など)では、打ち込んだ値がそのまま HTML に現れる。ここはその形を
|
||
* 直接置いて確かめる。ブラウザを通す経路の確認は
|
||
* browser.recording-secret.e2e.test.ts(http サーバ + 本物の Chromium)。
|
||
*/
|
||
function dump(html: string, selector?: string, depth = 3): string {
|
||
document.body.innerHTML = html;
|
||
return (0, eval)(buildDumpHtmlScript(selector, depth)) as string;
|
||
}
|
||
|
||
describe('buildDumpHtmlScript — value 属性に残った中身', () => {
|
||
it('パスワード欄の value 属性を伏せる', () => {
|
||
const out = dump('<input id="pw" type="password" name="password" value="hunter2-topsecret">');
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).toContain('value="[masked]"');
|
||
});
|
||
|
||
it('ふつうの欄の value 属性はそのまま残す(伏せ過ぎの回帰)', () => {
|
||
const out = dump('<input id="u" type="text" name="username" value="alice-visible">');
|
||
expect(out).toContain('value="alice-visible"');
|
||
expect(out).not.toContain('[masked]');
|
||
});
|
||
|
||
it('選んだ要素そのものが入力欄でも伏せる(根は querySelectorAll に出ない)', () => {
|
||
const out = dump(
|
||
'<input id="pw" type="password" name="password" value="hunter2-topsecret">',
|
||
'#pw',
|
||
);
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).toContain('value="[masked]"');
|
||
});
|
||
|
||
it('名札だけが手掛かりの欄も伏せる(「表示」で伏せ字を解いた欄)', () => {
|
||
const out = dump('<input id=":r3:" type="text" aria-label="パスワード" value="hunter2-topsecret">');
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).toContain('value="[masked]"');
|
||
});
|
||
|
||
it('`<label for>` が付いただけの欄も伏せる', () => {
|
||
const out = dump(
|
||
'<label for=":r3:">パスワード</label><input id=":r3:" type="text" value="hunter2-topsecret">',
|
||
);
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).toContain('value="[masked]"');
|
||
});
|
||
|
||
it('名札が取得範囲の外にあっても伏せる(複製の中だけを探さないこと)', () => {
|
||
// 取るのは `#field` だけ。名札はその外側にあるので、複製を作ってから
|
||
// 名札を探す実装では辿れず、パスワードが生の HTML に残る。
|
||
const out = dump(
|
||
'<label for=":r3:">パスワード</label>' +
|
||
'<div id="field"><input id=":r3:" type="text" value="hunter2-topsecret"></div>',
|
||
'#field',
|
||
);
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).toContain('value="[masked]"');
|
||
});
|
||
|
||
it('包んでいる `<label>` が取得範囲の外でも伏せる', () => {
|
||
// 取るのは `#field` だけ。複製は文書から切り離されるので、複製側で
|
||
// 祖先を辿ると `<label>` に届かず、パスワードが生の HTML に残る。
|
||
const out = dump(
|
||
'<label>パスワード<div id="field">' +
|
||
'<input id=":r7:" type="text" value="hunter2-topsecret"></div></label>',
|
||
'#field',
|
||
);
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).toContain('value="[masked]"');
|
||
});
|
||
|
||
it('入れ子の `<label>` に包まれた欄も伏せる', () => {
|
||
const out = dump('<label>パスワード<input id=":r4:" type="text" value="hunter2-topsecret"></label>');
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).toContain('value="[masked]"');
|
||
});
|
||
|
||
it('aria-labelledby で名札を指している欄も伏せる', () => {
|
||
const out = dump(
|
||
'<span id="lbl">パスワード</span>' +
|
||
'<input id=":r5:" type="text" aria-labelledby="lbl" value="hunter2-topsecret">',
|
||
);
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).toContain('value="[masked]"');
|
||
});
|
||
|
||
it('離れた名札がふつうの語なら伏せない(伏せ過ぎの回帰)', () => {
|
||
const out = dump(
|
||
'<label for=":r6:">Shipping address</label><input id=":r6:" type="text" value="tokyo-visible">',
|
||
);
|
||
expect(out).toContain('value="tokyo-visible"');
|
||
expect(out).not.toContain('[masked]');
|
||
});
|
||
|
||
it('autocomplete=one-time-code の欄も伏せる', () => {
|
||
const out = dump('<input id="c" type="text" autocomplete="one-time-code" value="482913">');
|
||
expect(out).not.toContain('482913');
|
||
expect(out).toContain('value="[masked]"');
|
||
});
|
||
|
||
it('textarea は中身(要素のテキスト)も伏せる', () => {
|
||
const out = dump('<textarea name="secretNote">hunter2-topsecret</textarea>');
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).toContain('[masked]');
|
||
});
|
||
|
||
it('伏せる欄とふつうの欄が並んでいても、伏せるのは片方だけ', () => {
|
||
const out = dump(
|
||
'<form><input name="username" value="alice-visible">' +
|
||
'<input name="password" type="password" value="hunter2-topsecret"></form>',
|
||
);
|
||
expect(out).toContain('value="alice-visible"');
|
||
expect(out).toContain('value="[masked]"');
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
});
|
||
|
||
it('複製で印の数がずれたら、印の無い欄まで含めて全部伏せる', () => {
|
||
// 複製したときに中身が増える独自要素を置く。`cloneNode(true)` は複製した
|
||
// 独自要素をその場で昇格させるので、構築子がもう一度走って複製側だけ欄が
|
||
// 増える(issue #1236 レビュー 3 巡目の原因そのもの)。
|
||
//
|
||
// ここでは構築子が「文書の中の欄を複製して自分に足す」形にしてある。
|
||
// 印は複製に乗って運ばれるので、構築子が新しい欄を作るだけなら数はずれ
|
||
// ない(印の付いた欄は元も複製も 1 つずつ)。ずれるのは、印を付けた**後**
|
||
// に走る構築子が印付きの欄を複製したとき——複製側にだけ印が 1 つ増える。
|
||
class DupField extends HTMLElement {
|
||
constructor() {
|
||
super();
|
||
const proto = document.getElementById('proto');
|
||
if (proto) this.appendChild(proto.cloneNode(true));
|
||
}
|
||
}
|
||
if (!customElements.get('dup-field')) customElements.define('dup-field', DupField);
|
||
|
||
const out = dump(
|
||
'<div id="field">' +
|
||
'<input id="proto" type="password" name="password" value="hunter2-topsecret">' +
|
||
'<dup-field></dup-field>' +
|
||
'<input name="username" value="alice-visible">' +
|
||
'</div>',
|
||
'#field',
|
||
5,
|
||
);
|
||
|
||
// 数が合わない=どれが秘密か分からない。通す側ではなく全部伏せる側へ倒す。
|
||
// 印の付いていなかった利用者名の欄まで伏せられていることが要(合っている
|
||
// ときは「伏せるのは片方だけ」で、この欄は素通しになる)。
|
||
expect(out).not.toContain('hunter2-topsecret');
|
||
expect(out).not.toContain('alice-visible');
|
||
expect(out).toContain('value="[masked]"');
|
||
// 印は書き出しに残さない。
|
||
expect(out).not.toContain('data-ao-mask');
|
||
});
|
||
|
||
it('切り詰めや除去はこれまでどおり効く', () => {
|
||
const out = dump('<div><script>var a=1</script><p><b><i>deep</i></b></p></div>', 'div', 1);
|
||
expect(out).not.toContain('var a=1');
|
||
expect(out).toContain('truncated');
|
||
});
|
||
});
|