Skip to content

test(blocks-screenshot): align mock workspace with scratch-blocks v2 API#637

Merged
takaokouji merged 3 commits into
feat/upstream-merge-2026-05from
fix/blocks-screenshot-getbubblecanvas
May 5, 2026
Merged

test(blocks-screenshot): align mock workspace with scratch-blocks v2 API#637
takaokouji merged 3 commits into
feat/upstream-merge-2026-05from
fix/blocks-screenshot-getbubblecanvas

Conversation

@takaokouji
Copy link
Copy Markdown

Summary

feat/upstream-merge-2026-05 で CI を 9 件失敗させていた blocks-screenshot.test.js のモックを scratch-blocks v2 の API に追従させる。本体 blocks-screenshot.js は既に workspace.getCanvas() / workspace.getBubbleCanvas() を使う v2 形式に移行済みで、テストモックだけが v1 の直接プロパティアクセス (svgBlockCanvas_ / svgBubbleCanvas_) のままだった。

Changes Made

  • makeMockWorkspace の戻り値を v2 メソッド形式に変更 (getCanvas / getBubbleCanvasjest.fn で公開)
  • テスト内で workspace.svgBubbleCanvas_ を直接参照していた 2 箇所を workspace.getBubbleCanvas() 経由 / workspace.getBubbleCanvas = jest.fn(() => null) に書き換え

Test Coverage

Test Suites: 1 passed, 1 total
Tests:       21 passed, 21 total

ローカルで全 21 テスト緑、Prettier も pass。

Related Issues

Closes #636

`blocks-screenshot.js` was already migrated to the v2 method-style API
(`workspace.getCanvas()` / `workspace.getBubbleCanvas()`), but the unit
test mock still exposed the v1 direct-property surface
(`svgBlockCanvas_` / `svgBubbleCanvas_`). 9 tests failed with
`TypeError: workspace.getBubbleCanvas is not a function` on
feat/upstream-merge-2026-05 CI.

Update `makeMockWorkspace` to return jest.fn-wrapped getters, and
replace the one inline `svgBubbleCanvas_` access with the equivalent
method call.

Closes #636
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

…th comments

Three coordinated fixes that together stop `toBlob()` from throwing
`SecurityError: Tainted canvases may not be exported.` when the workspace
contains the `@ruby:*` comments produced by the Ruby→Blocks converter:

1. Strip `<foreignObject>` from the BLOCK canvas clone, not only the
   bubble canvas. In scratch-blocks v2 (Blockly v12), block-attached
   comments are nested inside the block canvas and contain a
   `<foreignObject>` for the editing UI. Once the export SVG is loaded
   via `blob://`, those foreign objects taint the canvas.

2. Strip external `url(...)` references from injected `<style>` blocks
   via a new `stripExternalCssUrls` helper. The blocky stylesheet that
   carries the `blocklyText` color rules also contains
   `url(./static/blocks-media/.../sprites.png)` and friends — they're
   only used for cursors and toolbox sprite chrome, but the relative
   refs taint the rasteriser.

3. Drop hidden `<image>` elements that have an empty `href` (used as
   placeholders inside dynamic-block dropdowns, `display:none`). An
   empty href falls back to the document URL and tainted canvas under
   blob origin.

Adds unit tests for each piece and exports `stripExternalCssUrls` for
isolated testing.

Verified live: project with `puts(a.max)` / `t.sort!` etc. that emit
multiple `@ruby:*` comments now exports a PNG without errors.
@takaokouji
Copy link
Copy Markdown
Author

手動動作確認中に追加の bug を発見したため、本 issue / PR の scope に含めました。

追加で修正した問題

blocks-screenshot@ruby:* コメントを持つブロック (= Ruby converter が生成するすべての Smalruby ブロック) で実行すると SecurityError: Tainted canvases may not be exported. で失敗していた。

原因

3 つの canvas-taint 源があった:

  1. block canvas に残る <foreignObject>: scratch-blocks v2 ではブロック付属のコメントが g.blocklyBlockCanvas 内に配置され、その中に編集 UI 用の <foreignObject> を持つ。既存コードは bubble canvas のみ strip していたため、block canvas の foreignObject が残って blob 描画時に canvas を taint していた。
  2. インジェクト CSS の url() 参照: <style>blocklyText 系ルールには cursor / sprite 用の url("./static/blocks-media/...") も含まれている。これら相対パスが blob:// コンテキストで解決できず taint。
  3. href の hidden <image>: dynamic block 内のドロップダウンには display:none<image href=""> プレースホルダがあり、空 href は document URL (= blob URL) にフォールバックして taint。

修正

packages/scratch-gui/src/lib/blocks-screenshot.js:

  • <foreignObject> の strip を block canvas にも適用
  • stripExternalCssUrls ヘルパーで <style> から外部 url() を除去 (同一文書参照の url(#filter)url(data:) は保持)
  • inlineImageHrefs で空 href の <image>remove()

検証

  • ユニットテスト 29/29 緑 (foreignObject strip / stripExternalCssUrls / 空 href 除去 をそれぞれカバー)
  • ブラウザ実機で t = [3,1,2]; t.sort! 等の typical script に対し PNG 出力成功 (0 console errors)

dynamic block 実行確認

merge の影響を受ける各 dynamic block を実機で flag → 実行 → 状態確認:

機能 検証 結果
Array#sort! [3,1,2].sort! → list 値 [1,2,3]
Array#each [10,20,30].each { total += n } total = 60
Hash#keys + each {a:1,b:2,c:3}.keys.each { say k } keys list [:a,:b,:c], errors 0 ✅
String#reverse! "hello".reverse! _s_1_ = "olleh"

…t its joined string

`executeArrayMethod` (non-bang) reconstructed `items` from
`String(args.RECEIVER).split(' ')`. Scratch's `data_listcontents`
joins single-character items with NO separator (e.g. `[3,1,4,1,5,9,2,6]`
becomes `"31415926"`), so the split returned a single token and every
method (max/min/first/last/sort/join/reverse) returned that joined
string back rather than the computed value.

Walk the block tree from `util.thread.peekStack()` to find the
RECEIVER input block; if it's a `data_listcontents`, look up the list
directly via `target.lookupVariableById` / `lookupVariableByNameAndType`
and operate on `list.value`. Falls back to the space-split heuristic
for non-list receivers (e.g. literal strings).

Verified live: `arr = [3,1,4,1,5,9,2,6]` now reports
- arr.max = "9", arr.min = "1", arr.first = "3", arr.last = "6"
- arr.sort.join(",") = "1,1,2,3,4,5,6,9"
- arr.sort! mutates the list to [1,1,2,3,4,5,6,9]
- arr.join("-") = "1-1-2-3-4-5-6-9"
@takaokouji
Copy link
Copy Markdown
Author

さらに重要なバグ発見 — 配列メソッドの実行結果が壊れていた

「実行時にエラーがないことは当然として、結果が正しいことを確認してください」 という指示で実機検証したところ、新たな本番バグを発見したため scope に含めました。

現象

arr = [3, 1, 4, 1, 5, 9, 2, 6] のような単桁数値配列に対して:

期待値 修正前 修正後
arr.max "9" "31415926" "9"
arr.min "1" "31415926" "1"
arr.first "3" "31415926" "3"
arr.last "6" "31415926" "6"
arr.sort.join(",") "1,1,2,3,4,5,6,9" "31415926" "1,1,2,3,4,5,6,9"
arr.join("-") "1-1-2-3-4-5-6-9" "11234569" "1-1-2-3-4-5-6-9"

原因

scratch-vm/src/extensions/smalruby_ruby/method-executors.jsexecuteArrayMethod (非 bang) が args.RECEIVER を space-split で items に戻していた。Scratch の data_listcontents要素が全て 1 文字なら区切り無し で結合する仕様 ([3,1,4,1,5,9,2,6]"31415926")。空白で split しても 1 要素しか復元できないため、max/min/sort/join/first/last すべてが結合文字列をそのまま返していた。

修正

util.thread.peekStack() からカレントブロック ID を取得 → target.blocks.getBlock(id) で RECEIVER 入力先のブロックを取得 → data_listcontents なら target.lookupVariableById で list を直接ルックアップして list.value で操作。リストでない受信者 (リテラル文字列など) は従来通り space-split にフォールバック。

ハッシュ・文字列メソッドは多くが多文字なので影響を受けない (検証済み)。

動作確認

実機 Playwright で arr = [3,1,4,1,5,9,2,6] を含むスクリプトを実行し、グローバル変数に各結果を格納してチェック。テーブル全項目で正しい値を確認済み。

文字列・ハッシュ・bang 含む追加 18 ケースも全て期待値:

  • "hello world".reverse"dlrow olleh"
  • "hello world".gsub("world", "smalruby")"hello smalruby"
  • "abcd".reverse! (s2) → "dcba"
  • {a:10, b:20, c:30}.values.each { total += v }total = 60

既存 unit test (42 件) も全て pass。

@takaokouji takaokouji merged commit e15939b into feat/upstream-merge-2026-05 May 5, 2026
16 of 17 checks passed
@takaokouji takaokouji deleted the fix/blocks-screenshot-getbubblecanvas branch May 5, 2026 06:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant