Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions .agents/skills/note-article/scripts/capture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,24 @@ const IN_PAGE = `(() => {
落とさないと、押せる文字(click)と待てる文字(waitFor / expect)が
食い違う。DOM は「へやに入はいる」なので、シナリオに書いた
「へやに入る」で押せるのに待てない、という形になる。
innerText は行の切れ目を保つので、clone ではなく CSS で消す。 */
innerText は行の切れ目を保つので、clone ではなく表示のほうを消す。

⚠️ ここで <style> を差しこまないこと。艦隊のアプリは Zero-CDN のために
style-src 'self' を張っているので、差しこんだ 1 枚がそのつど
「Refused to apply inline style」になる。撮影そのものは通るが、
--strict は画面のエラーで落ちるので、**CSP を張っているアプリほど
--strict が使えなくなる**。要素の .style へ代入するぶんは CSP に
止められないので、rt / rp を 1 つずつ隠して、読み終わったら戻す。
(2026-09-02、さんすうブロックのマニュアル撮影で判明) */
const visibleText = () => {
const st = document.createElement('style');
st.textContent = 'rt,rp{display:none!important}';
(document.head || document.documentElement).appendChild(st);
const ruby = [...document.querySelectorAll('rt, rp')];
const prev = ruby.map((r) => r.style.display);
ruby.forEach((r) => { r.style.display = 'none'; });
try {
const t = document.body.innerText || '';
return t.replace(/\\n{2,}/g, '\\n').trim();
} finally {
st.remove();
ruby.forEach((r, i) => { r.style.display = prev[i]; });
}
};
return { norm, shown, clickables, findAll, visibleText, tap };
Expand Down
64 changes: 64 additions & 0 deletions .agents/skills/note-article/scripts/capture.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* 撮影の土台(capture.mjs)が、撮る相手の CSP を破らないことを見る。
*
* ── なぜ要るのか ────────────────────────────────
* 艦隊のアプリは Zero-CDN のために `style-src 'self'` を張っている。
* ページの中へ <style> を 1 枚差しこむと、そのつど
* 「Refused to apply inline style」がコンソールに出る。
* capture.mjs は画面のエラーを集めて --strict で異常終了するので、
* **CSP をきちんと張っているアプリほど --strict が使えなくなる**。
* しかも差しこんだ CSS は当たっていないので、消したかったふりがなも消えない。
*
* 2026-09-02、さんすうブロックのマニュアル撮影で実際に踏んだ。
* text() / has() / waitFor() / expect() が内側で visibleText() を呼ぶため、
* 画面を 1 度読むたびに 1 件ずつ増える。
*
* ⚠️ 要素の `.style` へ代入するぶんは CSP に止められない(HTML に書かれた
* style 属性だけが止まる)。だから「差しこまない」ほうへ倒す。
*
* ここはブラウザを起動せずに、ページの中で走る文字列そのものを読む。
* playwright が入っていない場所でも走る。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const HERE = dirname(fileURLToPath(import.meta.url));
const SRC = readFileSync(join(HERE, 'capture.mjs'), 'utf8');

/** ページの中へ送りこまれる文字列(IN_PAGE)だけを取り出す */
function inPage() {
const m = SRC.match(/const IN_PAGE = `([\s\S]*?)`;/);
assert.ok(m, 'IN_PAGE が見つからない。名前を変えたなら、この検査も直す');
return m[1];
}

test('ページの中へ <style> を差しこまない(style-src self を破らない)', () => {
const src = inPage();
assert.ok(
!/createElement\(\s*['"]style['"]\s*\)/.test(src),
'IN_PAGE が <style> を作っている。style-src \'self\' のアプリでは'
+ ' 1 回読むたびに CSP 違反が 1 件出て、--strict が使えなくなる',
);
assert.ok(
!/\badoptedStyleSheets\b|\binsertRule\b/.test(src),
'IN_PAGE がページのスタイルシートを触っている。要素の .style へ代入する形にする',
);
});

test('ふりがなは 要素の .style で隠して、読み終わったら戻す', () => {
const src = inPage();
assert.match(src, /querySelectorAll\('rt, rp'\)/,
'rt / rp を集めていない。visibleText() がふりがなを落とせていない');
assert.match(src, /style\.display = 'none'/,
'ふりがなを .style で隠していない');
assert.match(src, /finally \{[\s\S]*?style\.display = prev\[i\]/,
'隠したふりがなを戻していない。撮った絵からふりがなが消える');
});

test('ページ全体へ CSS を配る道具(freeze)を IN_PAGE に持ちこんでいない', () => {
const src = inPage();
assert.ok(!/animation-play-state/.test(src),
'IN_PAGE に freeze の CSS が入っている。freeze は addStyleTag の側の仕事');
});
18 changes: 13 additions & 5 deletions .claude/skills/note-article/scripts/capture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,24 @@ const IN_PAGE = `(() => {
落とさないと、押せる文字(click)と待てる文字(waitFor / expect)が
食い違う。DOM は「へやに入はいる」なので、シナリオに書いた
「へやに入る」で押せるのに待てない、という形になる。
innerText は行の切れ目を保つので、clone ではなく CSS で消す。 */
innerText は行の切れ目を保つので、clone ではなく表示のほうを消す。

⚠️ ここで <style> を差しこまないこと。艦隊のアプリは Zero-CDN のために
style-src 'self' を張っているので、差しこんだ 1 枚がそのつど
「Refused to apply inline style」になる。撮影そのものは通るが、
--strict は画面のエラーで落ちるので、**CSP を張っているアプリほど
--strict が使えなくなる**。要素の .style へ代入するぶんは CSP に
止められないので、rt / rp を 1 つずつ隠して、読み終わったら戻す。
(2026-09-02、さんすうブロックのマニュアル撮影で判明) */
const visibleText = () => {
const st = document.createElement('style');
st.textContent = 'rt,rp{display:none!important}';
(document.head || document.documentElement).appendChild(st);
const ruby = [...document.querySelectorAll('rt, rp')];
const prev = ruby.map((r) => r.style.display);
ruby.forEach((r) => { r.style.display = 'none'; });
try {
const t = document.body.innerText || '';
return t.replace(/\\n{2,}/g, '\\n').trim();
} finally {
st.remove();
ruby.forEach((r, i) => { r.style.display = prev[i]; });
}
};
return { norm, shown, clickables, findAll, visibleText, tap };
Expand Down
64 changes: 64 additions & 0 deletions .claude/skills/note-article/scripts/capture.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* 撮影の土台(capture.mjs)が、撮る相手の CSP を破らないことを見る。
*
* ── なぜ要るのか ────────────────────────────────
* 艦隊のアプリは Zero-CDN のために `style-src 'self'` を張っている。
* ページの中へ <style> を 1 枚差しこむと、そのつど
* 「Refused to apply inline style」がコンソールに出る。
* capture.mjs は画面のエラーを集めて --strict で異常終了するので、
* **CSP をきちんと張っているアプリほど --strict が使えなくなる**。
* しかも差しこんだ CSS は当たっていないので、消したかったふりがなも消えない。
*
* 2026-09-02、さんすうブロックのマニュアル撮影で実際に踏んだ。
* text() / has() / waitFor() / expect() が内側で visibleText() を呼ぶため、
* 画面を 1 度読むたびに 1 件ずつ増える。
*
* ⚠️ 要素の `.style` へ代入するぶんは CSP に止められない(HTML に書かれた
* style 属性だけが止まる)。だから「差しこまない」ほうへ倒す。
*
* ここはブラウザを起動せずに、ページの中で走る文字列そのものを読む。
* playwright が入っていない場所でも走る。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const HERE = dirname(fileURLToPath(import.meta.url));
const SRC = readFileSync(join(HERE, 'capture.mjs'), 'utf8');

/** ページの中へ送りこまれる文字列(IN_PAGE)だけを取り出す */
function inPage() {
const m = SRC.match(/const IN_PAGE = `([\s\S]*?)`;/);
assert.ok(m, 'IN_PAGE が見つからない。名前を変えたなら、この検査も直す');
return m[1];
}

test('ページの中へ <style> を差しこまない(style-src self を破らない)', () => {
const src = inPage();
assert.ok(
!/createElement\(\s*['"]style['"]\s*\)/.test(src),
'IN_PAGE が <style> を作っている。style-src \'self\' のアプリでは'
+ ' 1 回読むたびに CSP 違反が 1 件出て、--strict が使えなくなる',
);
assert.ok(
!/\badoptedStyleSheets\b|\binsertRule\b/.test(src),
'IN_PAGE がページのスタイルシートを触っている。要素の .style へ代入する形にする',
);
});

test('ふりがなは 要素の .style で隠して、読み終わったら戻す', () => {
const src = inPage();
assert.match(src, /querySelectorAll\('rt, rp'\)/,
'rt / rp を集めていない。visibleText() がふりがなを落とせていない');
assert.match(src, /style\.display = 'none'/,
'ふりがなを .style で隠していない');
assert.match(src, /finally \{[\s\S]*?style\.display = prev\[i\]/,
'隠したふりがなを戻していない。撮った絵からふりがなが消える');
});

test('ページ全体へ CSS を配る道具(freeze)を IN_PAGE に持ちこんでいない', () => {
const src = inPage();
assert.ok(!/animation-play-state/.test(src),
'IN_PAGE に freeze の CSS が入っている。freeze は addStyleTag の側の仕事');
});
Loading