143 lines
6.4 KiB
TypeScript
143 lines
6.4 KiB
TypeScript
// @vitest-environment jsdom
|
||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||
import { SNAPSHOT_SCRIPT } from './browser.js';
|
||
|
||
/**
|
||
* SNAPSHOT_SCRIPT はページ内で eval される文字列。jsdom の上で実際に
|
||
* 走らせて、書き出しにパスワードが載らないことを見る(issue #1236)。
|
||
*
|
||
* jsdom の getBoundingClientRect は常に 0×0 を返すので、そのままだと
|
||
* isVisible() が全要素を落として何も走査されない。差し替えておく。
|
||
*/
|
||
let originalRect: () => DOMRect;
|
||
|
||
beforeAll(() => {
|
||
originalRect = Element.prototype.getBoundingClientRect;
|
||
Element.prototype.getBoundingClientRect = function (): DOMRect {
|
||
return { width: 100, height: 20, top: 0, left: 0, right: 100, bottom: 20, x: 0, y: 0, toJSON() {} } as DOMRect;
|
||
};
|
||
});
|
||
|
||
afterAll(() => {
|
||
Element.prototype.getBoundingClientRect = originalRect;
|
||
});
|
||
|
||
/** html を置いてから values を実際の .value に流し込み、書き出しを取る。 */
|
||
function snapshot(html: string, values: Record<string, string> = {}): string {
|
||
document.body.innerHTML = html;
|
||
for (const [id, v] of Object.entries(values)) {
|
||
(document.getElementById(id) as HTMLInputElement | HTMLTextAreaElement).value = v;
|
||
}
|
||
const result = (0, eval)(SNAPSHOT_SCRIPT) as { text: string };
|
||
return result.text;
|
||
}
|
||
|
||
describe('SNAPSHOT_SCRIPT — 入力欄の中身', () => {
|
||
it('type=password の欄は中身を書き出さず、値があることだけ残す', () => {
|
||
const text = snapshot('<input id="pw" type="password" name="password">', { pw: 'hunter2-topsecret' });
|
||
expect(text).not.toContain('hunter2-topsecret');
|
||
expect(text).toContain('value=[masked]');
|
||
});
|
||
|
||
it('name からパスワードと分かる欄も伏せる(type=text でも)', () => {
|
||
const text = snapshot('<input id="p" type="text" name="loginPassword">', { p: 'correct-horse-battery' });
|
||
expect(text).not.toContain('correct-horse-battery');
|
||
expect(text).toContain('value=[masked]');
|
||
});
|
||
|
||
it('autocomplete=one-time-code の欄も伏せる', () => {
|
||
const text = snapshot('<input id="c" type="text" autocomplete="one-time-code">', { c: '482913' });
|
||
expect(text).not.toContain('482913');
|
||
expect(text).toContain('value=[masked]');
|
||
});
|
||
|
||
it('空のパスワード欄には印を出さない(値の有無だけが残る)', () => {
|
||
const text = snapshot('<input id="pw" type="password" name="password">');
|
||
expect(text).not.toContain('[masked]');
|
||
});
|
||
|
||
it('名札だけが手掛かりの欄も伏せる(「表示」で伏せ字を解いた欄)', () => {
|
||
// type は text に変わり、id は枠組みの機械生成。残る手掛かりは名札だけ。
|
||
const text = snapshot('<input id=":r3:" type="text" aria-label="Password">', {
|
||
':r3:': 'correct-horse-battery',
|
||
});
|
||
expect(text).not.toContain('correct-horse-battery');
|
||
expect(text).toContain('value=[masked]');
|
||
});
|
||
|
||
it('`<label for>` が付いただけの欄も伏せる(伏せ字を解いた欄)', () => {
|
||
// type=text・name 無し・機械生成の id。手掛かりは離れた場所の名札だけ。
|
||
const text = snapshot('<label for=":r3:">パスワード</label><input id=":r3:" type="text">', {
|
||
':r3:': 'correct-horse-battery',
|
||
});
|
||
expect(text).not.toContain('correct-horse-battery');
|
||
expect(text).toContain('value=[masked]');
|
||
});
|
||
|
||
it('入れ子の `<label>` に包まれた欄も伏せる', () => {
|
||
const text = snapshot('<label>パスワード<input id=":r4:" type="text"></label>', {
|
||
':r4:': 'correct-horse-battery',
|
||
});
|
||
expect(text).not.toContain('correct-horse-battery');
|
||
expect(text).toContain('value=[masked]');
|
||
});
|
||
|
||
it('aria-labelledby で名札を指している欄も伏せる', () => {
|
||
const text = snapshot(
|
||
'<span id="lbl">パスワード</span><input id=":r5:" type="text" aria-labelledby="lbl">',
|
||
{ ':r5:': 'correct-horse-battery' },
|
||
);
|
||
expect(text).not.toContain('correct-horse-battery');
|
||
expect(text).toContain('value=[masked]');
|
||
});
|
||
|
||
it('離れた名札がふつうの語なら伏せない(伏せ過ぎの回帰)', () => {
|
||
const text = snapshot('<label for=":r6:">Shipping address</label><input id=":r6:" type="text">', {
|
||
':r6:': '東京都千代田区',
|
||
});
|
||
expect(text).toContain('value="東京都千代田区"');
|
||
});
|
||
|
||
it('日本語の名札でも伏せる', () => {
|
||
const text = snapshot('<input id="x1" type="text" placeholder="パスワード">', {
|
||
x1: 'correct-horse-battery',
|
||
});
|
||
expect(text).not.toContain('correct-horse-battery');
|
||
expect(text).toContain('value=[masked]');
|
||
});
|
||
|
||
it('ふつうの名札の欄は伏せない(伏せ過ぎの回帰)', () => {
|
||
const text = snapshot('<input id="q" type="text" aria-label="検索">', { q: 'ラーメン' });
|
||
expect(text).toContain('value="ラーメン"');
|
||
});
|
||
|
||
it('ふつうの入力欄はこれまでどおり中身が載る(伏せ過ぎの回帰)', () => {
|
||
const text = snapshot('<input id="u" type="text" name="username">', { u: 'alice' });
|
||
expect(text).toContain('value="alice"');
|
||
});
|
||
|
||
it('最初から中身が入っている textarea が秘密の欄なら、その文字を書き出さない', () => {
|
||
// textarea の中身は要素のテキストそのもの。value=[masked] だけ直しても、
|
||
// 名札の代わり(elementName)と本文の走査(walk)から同じ文字が 2 回出る。
|
||
const text = snapshot('<textarea id="ta" name="apiSecret">sk-live-PREFILLED-TEXTAREA-123</textarea>');
|
||
expect(text).not.toContain('sk-live-PREFILLED-TEXTAREA-123');
|
||
expect(text).toContain('value=[masked]');
|
||
});
|
||
|
||
it('ふつうの textarea の中身はこれまでどおり載る(伏せ過ぎの回帰)', () => {
|
||
const text = snapshot('<textarea id="c" name="comment">きょうの下書き-visible</textarea>');
|
||
expect(text).toContain('きょうの下書き-visible');
|
||
expect(text).not.toContain('[masked]');
|
||
});
|
||
|
||
it('パスワード欄とふつうの欄が並んでいても、伏せるのは片方だけ', () => {
|
||
const text = snapshot(
|
||
'<form><input id="u" name="username"><input id="pw" type="password" name="password"></form>',
|
||
{ u: 'alice', pw: 'hunter2-topsecret' },
|
||
);
|
||
expect(text).toContain('value="alice"');
|
||
expect(text).toContain('value=[masked]');
|
||
expect(text).not.toContain('hunter2-topsecret');
|
||
});
|
||
});
|