diff --git a/docs/tutorial/improvement-plan.md b/docs/tutorial/improvement-plan.md index 8c0bdfac7e..eb3e521ff8 100644 --- a/docs/tutorial/improvement-plan.md +++ b/docs/tutorial/improvement-plan.md @@ -8,6 +8,8 @@ Welcome モーダルの `cardBlocks` / `cardRuby` / `cardMesh` が提示する 3 ユーザーが Welcome → チュートリアル一覧 → カテゴリで学習軸を選ぶ → 同じ題材を Lv1 (コード挿入) → Lv2 (ブロック手組み) → Lv3 (Ruby) と難度別に進めるという既存パターンを、全軸で踏襲する。 +ただし **`gettingStarted` (「さあ、始めましょう」) は例外**で、Lv 分けをせず 1 本完結の単機能チュートリアルを並べる (→「方針分岐: `gettingStarted` は単機能・Lv 分けなし」)。 + ## 現状サマリー (2026-05 時点) ### 既存 deck 一覧 (`packages/scratch-gui/src/lib/libraries/decks/index.jsx`) @@ -83,6 +85,22 @@ export const CATEGORIES = { 実装時、`library.jsx` で `withCategories` 表示の順序が `CATEGORIES` オブジェクトのキー順に依存しないなら、明示的な順序配列を導入する。 +### 方針分岐: `gettingStarted` は単機能・Lv 分けなし + +上の「ゴール」で述べた **Lv1 / Lv2 / Lv3 の難度別 3 本立て**は、`meshStep1-3` や Block 軸 (Lv0/Lv2/Lv3) のように **同じ題材を段階的に深める軸**にだけ適用する。**`gettingStarted` はこの体系から明示的に外す**: + +| 観点 | Lv 別の軸 (mesh / block / ruby) | `gettingStarted` | +|---|---|---| +| 1 deck の役割 | 同じ題材を難度別に 3 本 | **1 チュートリアル = 1 機能**で完結 | +| ステップ数 | 5〜8 | **3〜6** | +| 順番 | Lv1 → Lv2 → Lv3 の前提あり | **順番に依存しない**(どれから始めてもよい) | +| 最終ステップ | `deckIds` で次の Lv へ誘導 | **`deckIds` を持たない**(他 deck に依存させない) | +| deck id | `chat-1-basic-1` のように Lv 番号入り | `getting-started-<機能スラッグ>`(**番号を付けない**) | + +理由は授業運用にある。中学校技術科「双方向性のあるコンテンツ」の単元では、ワークシートから **「『さあ、始めましょう』から好きなものを 3 つやってみよう」**と指示したい。Lv 分けや通し番号があると「順番にやるもの」に見えてしまい、この指示が成立しない。 + +`gettingStarted` の単機能 deck の命名規約 (deck id / locale キー / step 画像 / 画像キー / `urlId`) は `packages/scratch-gui/src/lib/libraries/decks/categories/getting-started.jsx` の冒頭コメントに定義してあり、後続の deck 追加もそれに従う。 + ## Phase 1: Mesh 再カテゴリ化 **目標**: 既存 9 deck を 3 カテゴリ × 3 lv に再分類。新規 deck・画像は無し。 diff --git a/docs/tutorial/screenshots/0201-tips-library-1280x800.png b/docs/tutorial/screenshots/0201-tips-library-1280x800.png index d8fcdf2a41..ab6885876d 100644 Binary files a/docs/tutorial/screenshots/0201-tips-library-1280x800.png and b/docs/tutorial/screenshots/0201-tips-library-1280x800.png differ diff --git a/packages/scratch-gui/src/lib/libraries/decks/categories/getting-started.jsx b/packages/scratch-gui/src/lib/libraries/decks/categories/getting-started.jsx index 9368b2ce17..bb47194dc5 100644 --- a/packages/scratch-gui/src/lib/libraries/decks/categories/getting-started.jsx +++ b/packages/scratch-gui/src/lib/libraries/decks/categories/getting-started.jsx @@ -2,8 +2,20 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import libraryIntro from '../thumbnails/getting-started.jpg'; +import libraryGettingStartedCostume from '../thumbnails/getting-started-costume.jpg'; +import libraryGettingStartedSprites from '../thumbnails/getting-started-sprites.jpg'; import {CATEGORIES} from '../../tutorial-tags'; +import GreenFlagIcon from '../green-flag-icon.jsx'; +// 単機能チュートリアルの規約 (issue #1179 で確定。後続 leaf もこれに従う): +// - deck id : `getting-started-<機能スラッグ>` — Lv 番号も通し番号も付けない。 +// このカテゴリの deck は 1 本完結・順番に依存しない +// (「好きなものを3つ」が成立する)ため、順序を示す番号は付けない +// - locale キー : `gui.howtos.getting-started-<スラッグ>.{name,stepN.title}` +// - step 画像 : `getting-started-<スラッグ>--<説明>.`。 +// 複数 deck で共有する素材は `getting-started-shared-<説明>.` +// - 画像キー : `gettingStarted<スラッグPascal>StepN`(ja-steps.js / en-steps.js) +// - urlId : `gettingStarted<スラッグPascal>` const decks = { 'intro-getting-started': { name: ( @@ -71,6 +83,165 @@ end`, ], urlId: 'getStarted' }, + + // ─── コスチュームをかえてみよう(単機能・見た目) ─────────────────────── + 'getting-started-costume': { + name: ( + + ), + tags: ['ブロック', 'はじめて'], + category: CATEGORIES.gettingStarted, + img: libraryGettingStartedCostume, + nameMessageId: 'gui.howtos.getting-started-costume.name', + // setup.tab は指定しない。step1 の「チュートリアルをはじめる」が新規プロジェクトを + // 作り直してコードタブに戻すため、ここで costumes を指定しても学習者が最初の + // ボタンを押した瞬間に打ち消され、一瞬だけタブが切り替わる不自然な動きになる + // (レビュー指摘 #1179)。コスチュームタブへは step2 の説明で誘導する。 + allowedBlocks: { + motion: [], + looks: ['looks_nextcostume', 'looks_switchcostumeto', 'looks_costume'], + sound: [], + event: ['event_whenflagclicked'], + control: ['control_repeat', 'control_wait'], + sensing: [], + operators: [], + }, + steps: [ + { + title: ( + + ), + image: 'gettingStartedCostumeStep1', + startTutorial: true, + animationTarget: 'startTutorialButton', + }, + { + title: ( + + ), + image: 'gettingStartedCostumeStep2', + animationTarget: 'nextButton', + }, + { + title: ( + }} + /> + ), + image: 'gettingStartedCostumeStep3', + code: `when_flag_clicked do + 3.times do + next_costume + sleep(1) + end +end`, + codeType: 'blocks', + animationTarget: 'insertCodeButton', + }, + ], + urlId: 'gettingStartedCostume', + }, + + // ─── スプライトを2つ使ってみよう(単機能・スプライト操作) ─────────────── + 'getting-started-sprites': { + name: ( + + ), + tags: ['ブロック', 'はじめて'], + category: CATEGORIES.gettingStarted, + img: libraryGettingStartedSprites, + nameMessageId: 'gui.howtos.getting-started-sprites.name', + // スプライトの追加・選択が主題なので、コードタブから始める。 + setup: { + tab: 'code', + }, + allowedBlocks: { + motion: [], + looks: ['looks_sayforsecs', 'looks_say'], + sound: [], + event: ['event_whenflagclicked'], + control: ['control_wait'], + sensing: [], + operators: [], + }, + steps: [ + { + title: ( + + ), + image: 'gettingStartedSpritesStep1', + startTutorial: true, + animationTarget: 'startTutorialButton', + }, + { + title: ( + + ), + image: 'gettingStartedSpritesStep2', + animationTarget: 'nextButton', + }, + { + title: ( + + ), + image: 'gettingStartedSpritesStep3', + code: `when_flag_clicked do + say("こんにちは!", 2) +end`, + codeType: 'blocks', + animationTarget: 'insertCodeButton', + }, + { + title: ( + }} + /> + ), + image: 'gettingStartedSpritesStep4', + code: `when_flag_clicked do + sleep(2) + say("やあ!", 2) +end`, + codeType: 'blocks', + animationTarget: 'insertCodeButton', + }, + ], + urlId: 'gettingStartedSprites', + }, }; export default decks; diff --git a/packages/scratch-gui/src/lib/libraries/decks/categories/getting-started.locale.js b/packages/scratch-gui/src/lib/libraries/decks/categories/getting-started.locale.js index 9e796279e7..c169737768 100644 --- a/packages/scratch-gui/src/lib/libraries/decks/categories/getting-started.locale.js +++ b/packages/scratch-gui/src/lib/libraries/decks/categories/getting-started.locale.js @@ -11,6 +11,19 @@ export const ja = { 'gui.howtos.getting-started.step.bounceCode': 'ルビーを表示してプログラムを入力してみよう', 'gui.howtos.getting-started.step.runRuby': '実行ボタンを押してプログラムを実行しよう', 'gui.howtos.getting-started.step.firstProgram': 'ネコが行ったり来たりするプログラムができました🎉', + // 単機能チュートリアル (#1179) + 'gui.howtos.getting-started-costume.name': 'コスチュームをかえてみよう', + 'gui.howtos.getting-started-costume.step1.title': 'スプライトの見た目(コスチューム)をかえてみよう!', + 'gui.howtos.getting-started-costume.step2.title': '「コスチューム」タブで、コスチュームをふやしてみよう', + 'gui.howtos.getting-started-costume.step3.title': + '「コード」タブでこのプログラムを入れて、{greenFlag}を押してみよう', + 'gui.howtos.getting-started-sprites.name': 'スプライトを2つ使ってみよう', + 'gui.howtos.getting-started-sprites.step1.title': 'スプライトをふやして、2つのキャラクターを動かそう!', + 'gui.howtos.getting-started-sprites.step2.title': + 'スプライトリストの右下のボタンから、ペンギンをふやしてみよう', + 'gui.howtos.getting-started-sprites.step3.title': 'ネコ(スプライト1)をえらんでから、このプログラムを入れよう', + 'gui.howtos.getting-started-sprites.step4.title': + 'ペンギンをえらぶとプログラムが切りかわるよ。入れたら{greenFlag}を押してみよう', }; export const jaHira = { @@ -18,6 +31,20 @@ export const jaHira = { 'gui.howtos.getting-started.step.bounceCode': 'ルビーをひょうじしてプログラムをにゅうりょくしてみよう', 'gui.howtos.getting-started.step.runRuby': 'じっこうボタンをおしてプログラムをじっこうしよう', 'gui.howtos.getting-started.step.firstProgram': 'ネコがいったりきたりするプログラムができました🎉', + // 単機能チュートリアル (#1179) + 'gui.howtos.getting-started-costume.name': 'コスチュームをかえてみよう', + 'gui.howtos.getting-started-costume.step1.title': 'スプライトのみため(コスチューム)をかえてみよう!', + 'gui.howtos.getting-started-costume.step2.title': '「コスチューム」タブで、コスチュームをふやしてみよう', + 'gui.howtos.getting-started-costume.step3.title': + '「コード」タブでこのプログラムをいれて、{greenFlag}をおしてみよう', + 'gui.howtos.getting-started-sprites.name': 'スプライトを2つつかってみよう', + 'gui.howtos.getting-started-sprites.step1.title': 'スプライトをふやして、2つのキャラクターをうごかそう!', + 'gui.howtos.getting-started-sprites.step2.title': + 'スプライトリストのみぎしたのボタンから、ペンギンをふやしてみよう', + 'gui.howtos.getting-started-sprites.step3.title': + 'ネコ(スプライト1)をえらんでから、このプログラムをいれよう', + 'gui.howtos.getting-started-sprites.step4.title': + 'ペンギンをえらぶとプログラムがきりかわるよ。いれたら{greenFlag}をおしてみよう', }; export const en = { @@ -29,4 +56,16 @@ export const en = { 'gui.howtos.getting-started.step.bounceCode': 'Show Ruby and enter your program', 'gui.howtos.getting-started.step.runRuby': 'Press the run button to execute the program', 'gui.howtos.getting-started.step.firstProgram': "You've made a program where the cat moves back and forth! 🎉", + // Single-feature tutorials (#1179) + 'gui.howtos.getting-started-costume.name': 'Switch costumes', + 'gui.howtos.getting-started-costume.step1.title': "Let's change how your sprite looks (its costume)!", + 'gui.howtos.getting-started-costume.step2.title': 'Add another costume in the Costumes tab', + 'gui.howtos.getting-started-costume.step3.title': 'In the Code tab, insert this program and press {greenFlag}', + 'gui.howtos.getting-started-sprites.name': 'Use two sprites', + 'gui.howtos.getting-started-sprites.step1.title': "Add a sprite and let's move two characters!", + 'gui.howtos.getting-started-sprites.step2.title': + 'Add a penguin with the button at the bottom right of the sprite list', + 'gui.howtos.getting-started-sprites.step3.title': 'Select the cat (Sprite1) first, then insert this program', + 'gui.howtos.getting-started-sprites.step4.title': + 'Selecting the penguin switches the program. Insert it and press {greenFlag}', }; diff --git a/packages/scratch-gui/src/lib/libraries/decks/en-steps.js b/packages/scratch-gui/src/lib/libraries/decks/en-steps.js index e09179c26e..2000ef3895 100644 --- a/packages/scratch-gui/src/lib/libraries/decks/en-steps.js +++ b/packages/scratch-gui/src/lib/libraries/decks/en-steps.js @@ -4,6 +4,13 @@ import introBounceCode from './steps/intro-2-bounce-code.ja.gif'; import introRunRuby from './steps/intro-3-run-ruby.ja.gif'; import introFirstProgram from './steps/intro-4-first-program.gif'; +// Getting Started 単機能チュートリアル (issue #1179)。ja と同じ素材を使う +// (UI 操作の GIF は言語非依存)。 +import gettingStartedCostumeList from './steps/getting-started-costume-1-costume-list.png'; +import gettingStartedCostumeProgram from './steps/getting-started-costume-3-program.png'; +import gettingStartedSpritesCatProgram from './steps/getting-started-sprites-3-cat-program.png'; +import gettingStartedSpritesPenguinProgram from './steps/getting-started-sprites-4-penguin-program.png'; + // Chat Tutorial 1 Basic 1: メッセージを送ってみよう import chat1Basic1Step1 from './steps/chat1-basic1-1-intro.gif'; import chat1Basic1Step2 from './steps/chat1-basic1-2-run-code.gif'; @@ -121,6 +128,15 @@ const enImages = { introBounceCode: introBounceCode, introRunRuby: introRunRuby, introFirstProgram: introFirstProgram, + // Getting Started: コスチュームをかえてみよう (#1179) + gettingStartedCostumeStep1: gettingStartedCostumeList, + gettingStartedCostumeStep2: chat3Mesh1Step4, + gettingStartedCostumeStep3: gettingStartedCostumeProgram, + // Getting Started: スプライトを2つ使ってみよう (#1179) + gettingStartedSpritesStep1: chat2Sprites1Step1, + gettingStartedSpritesStep2: chat2Sprites1Step5, + gettingStartedSpritesStep3: gettingStartedSpritesCatProgram, + gettingStartedSpritesStep4: gettingStartedSpritesPenguinProgram, // Chat Tutorial 1 Basic 1 chat1Basic1Step1: chat1Basic1Step1, chat1Basic1Step2: chat1Basic1Step2, diff --git a/packages/scratch-gui/src/lib/libraries/decks/ja-steps.js b/packages/scratch-gui/src/lib/libraries/decks/ja-steps.js index 6f8c65d90f..0d8b185fdf 100644 --- a/packages/scratch-gui/src/lib/libraries/decks/ja-steps.js +++ b/packages/scratch-gui/src/lib/libraries/decks/ja-steps.js @@ -4,6 +4,15 @@ import introBounceCode from './steps/intro-2-bounce-code.ja.gif'; import introRunRuby from './steps/intro-3-run-ruby.ja.gif'; import introFirstProgram from './steps/intro-4-first-program.gif'; +// Getting Started 単機能チュートリアル (issue #1179)。UI 操作は既存 GIF を流用し +// (.claude/rules/scratch-gui/tutorial.md「画像の流用ルール」)、静止画は既存 GIF の +// フレームから生成した派生 PNG を使う。挿入するプログラムのステップは、実際に +// 生成されるブロックを「命令ブロックを画像として保存」で書き出したものを使う。 +import gettingStartedCostumeList from './steps/getting-started-costume-1-costume-list.png'; +import gettingStartedCostumeProgram from './steps/getting-started-costume-3-program.png'; +import gettingStartedSpritesCatProgram from './steps/getting-started-sprites-3-cat-program.png'; +import gettingStartedSpritesPenguinProgram from './steps/getting-started-sprites-4-penguin-program.png'; + // Chat Tutorial 1 Basic 1: メッセージを送ってみよう import chat1Basic1Step1 from './steps/chat1-basic1-1-intro.gif'; import chat1Basic1Step2 from './steps/chat1-basic1-2-run-code.gif'; @@ -122,6 +131,17 @@ const jaImages = { introBounceCode: introBounceCode, introRunRuby: introRunRuby, introFirstProgram: introFirstProgram, + // Getting Started: コスチュームをかえてみよう (#1179) + // step1 はできあがりのコスチューム一覧、step2 はコスチュームを追加する操作 GIF、 + // step3 は挿入されるプログラムそのもの。各画像が step の説明と一致すること。 + gettingStartedCostumeStep1: gettingStartedCostumeList, + gettingStartedCostumeStep2: chat3Mesh1Step4, + gettingStartedCostumeStep3: gettingStartedCostumeProgram, + // Getting Started: スプライトを2つ使ってみよう (#1179) + gettingStartedSpritesStep1: chat2Sprites1Step1, + gettingStartedSpritesStep2: chat2Sprites1Step5, + gettingStartedSpritesStep3: gettingStartedSpritesCatProgram, + gettingStartedSpritesStep4: gettingStartedSpritesPenguinProgram, // Chat Tutorial 1 Basic 1 chat1Basic1Step1: chat1Basic1Step1, chat1Basic1Step2: chat1Basic1Step2, diff --git a/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-costume-1-costume-list.png b/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-costume-1-costume-list.png new file mode 100644 index 0000000000..e8fefb8210 Binary files /dev/null and b/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-costume-1-costume-list.png differ diff --git a/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-costume-3-program.png b/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-costume-3-program.png new file mode 100644 index 0000000000..6df80eba99 Binary files /dev/null and b/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-costume-3-program.png differ diff --git a/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-sprites-3-cat-program.png b/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-sprites-3-cat-program.png new file mode 100644 index 0000000000..702f1efd71 Binary files /dev/null and b/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-sprites-3-cat-program.png differ diff --git a/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-sprites-4-penguin-program.png b/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-sprites-4-penguin-program.png new file mode 100644 index 0000000000..3f4c993181 Binary files /dev/null and b/packages/scratch-gui/src/lib/libraries/decks/steps/getting-started-sprites-4-penguin-program.png differ diff --git a/packages/scratch-gui/src/lib/libraries/decks/thumbnails/getting-started-costume.jpg b/packages/scratch-gui/src/lib/libraries/decks/thumbnails/getting-started-costume.jpg new file mode 100644 index 0000000000..df2d6aac47 Binary files /dev/null and b/packages/scratch-gui/src/lib/libraries/decks/thumbnails/getting-started-costume.jpg differ diff --git a/packages/scratch-gui/src/lib/libraries/decks/thumbnails/getting-started-sprites.jpg b/packages/scratch-gui/src/lib/libraries/decks/thumbnails/getting-started-sprites.jpg new file mode 100644 index 0000000000..481ebb76e8 Binary files /dev/null and b/packages/scratch-gui/src/lib/libraries/decks/thumbnails/getting-started-sprites.jpg differ diff --git a/packages/scratch-gui/test/unit/containers/use-teacher-notifications.test.js b/packages/scratch-gui/test/unit/containers/use-teacher-notifications.test.js index df16f44efd..396ddb7df6 100644 --- a/packages/scratch-gui/test/unit/containers/use-teacher-notifications.test.js +++ b/packages/scratch-gui/test/unit/containers/use-teacher-notifications.test.js @@ -107,8 +107,10 @@ describe('useTeacherNotifications (EPIC #1111 / 日次取得)', () => { ); mockListNotifications.mockResolvedValue({ notifications: [notice('n1')], unreadCount: 1 }); const { result } = renderHook(() => useTeacherNotifications({ idToken: TOKEN, handleTeacher401 })); - await waitFor(() => expect(mockListNotifications).toHaveBeenCalledTimes(1)); - expect(result.current.unreadCount).toBe(1); + // API 呼び出しは state 反映より先に観測できるので、呼び出し回数ではなく + // 反映後の state を待つ(呼び出しで待つと CI で state 反映前に進んで落ちる)。 + await waitFor(() => expect(result.current.unreadCount).toBe(1)); + expect(mockListNotifications).toHaveBeenCalledTimes(1); // 空キーではキャッシュを書かない(他の空キー先生と共有されないように)。 const cache = JSON.parse(window.localStorage.getItem(STORAGE_KEY)); expect(cache.notifications[0].notificationId).toBe('x'); diff --git a/packages/scratch-gui/test/unit/lib/libraries/decks/getting-started-decks.test.js b/packages/scratch-gui/test/unit/lib/libraries/decks/getting-started-decks.test.js new file mode 100644 index 0000000000..5aac258554 --- /dev/null +++ b/packages/scratch-gui/test/unit/lib/libraries/decks/getting-started-decks.test.js @@ -0,0 +1,127 @@ +/** + * Structural + locale + conversion coverage for the single-feature + * "さあ、始めましょう" (gettingStarted) tutorial decks. + * + * These decks are content, so the meaningful regressions are: + * 1. deck wiring — category / urlId / step count / no cross-deck dependency + * (the category promises "好きなものを3つ", so a deck must not require + * another deck to be finished first) + * 2. locale completeness — every FormattedMessage id resolves in ja / ja-Hira / en + * 3. asset wiring — every step image key resolves in both ja and en image maps + * 4. runnability — every step's `code` snippet actually converts to blocks + * + * See docs/tutorial/improvement-plan.md and issues #1177 / #1179. + */ +import decks from '../../../../../src/lib/libraries/decks/index.jsx'; +import {jaImages} from '../../../../../src/lib/libraries/decks/ja-steps'; +import {enImages} from '../../../../../src/lib/libraries/decks/en-steps'; +import {CATEGORIES} from '../../../../../src/lib/libraries/tutorial-tags'; +import ja from '../../../../../src/locales/ja'; +import jaHira from '../../../../../src/locales/ja-Hira'; +import en from '../../../../../src/locales/en'; +import RubyToBlocksConverter from '../../../../../src/lib/ruby-to-blocks-converter'; + +const LOCALES = {ja, 'ja-Hira': jaHira, en}; + +// The single-feature decks added by #1179. `intro-getting-started` is the +// pre-existing deck and is guarded separately (it is a Ruby intro, not a +// single-feature block deck). +const SINGLE_FEATURE_DECK_IDS = ['getting-started-costume', 'getting-started-sprites']; + +const collectMessageIds = deck => { + const ids = []; + if (deck.name && deck.name.props && deck.name.props.id) { + ids.push(deck.name.props.id); + } + deck.steps.forEach(step => { + if (step.title && step.title.props && step.title.props.id) { + ids.push(step.title.props.id); + } + }); + return ids; +}; + +describe('Getting Started single-feature tutorial decks', () => { + test('the pre-existing intro deck is untouched and still first', () => { + expect(decks['intro-getting-started']).toBeDefined(); + expect(Object.keys(decks)[0]).toBe('intro-getting-started'); + }); + + test('both decks exist under the gettingStarted category', () => { + SINGLE_FEATURE_DECK_IDS.forEach(id => { + expect(decks[id]).toBeDefined(); + expect(decks[id].category).toBe(CATEGORIES.gettingStarted); + }); + }); + + describe.each(SINGLE_FEATURE_DECK_IDS)('%s', deckId => { + let deck; + beforeEach(() => { + deck = decks[deckId]; + }); + + test('has a urlId and 3-6 steps (single feature,完結する長さ)', () => { + expect(typeof deck.urlId).toBe('string'); + expect(deck.urlId.length).toBeGreaterThan(0); + expect(Array.isArray(deck.steps)).toBe(true); + expect(deck.steps.length).toBeGreaterThanOrEqual(3); + expect(deck.steps.length).toBeLessThanOrEqual(6); + }); + + test('does not chain to another deck (order-independent)', () => { + deck.steps.forEach(step => { + expect(step.deckIds).toBeUndefined(); + }); + }); + + test('every message id resolves in ja / ja-Hira / en', () => { + const ids = collectMessageIds(deck); + expect(ids.length).toBeGreaterThan(0); + const missing = []; + ids.forEach(id => { + Object.entries(LOCALES).forEach(([locale, table]) => { + if (!Object.prototype.hasOwnProperty.call(table, id)) { + missing.push(`${id} missing in ${locale}`); + } + }); + }); + expect(missing).toEqual([]); + }); + + test('has a thumbnail and every step image key resolves in ja / en image maps', () => { + expect(deck.img).toBeTruthy(); + const missing = []; + deck.steps.forEach(step => { + expect(typeof step.image).toBe('string'); + if (!Object.prototype.hasOwnProperty.call(jaImages, step.image)) { + missing.push(`${step.image} missing in jaImages`); + } + if (!Object.prototype.hasOwnProperty.call(enImages, step.image)) { + missing.push(`${step.image} missing in enImages`); + } + }); + expect(missing).toEqual([]); + }); + + test('no two steps reuse the same image key (説明と絵がズレるのを防ぐ)', () => { + // jest は画像 import をすべて同じスタブに解決するので、解決後の値では + // なくキーの重複を見る(ja-steps.js のキー → ファイルは 1:1 で運用する)。 + const keys = deck.steps.map(step => step.image); + expect(new Set(keys).size).toBe(keys.length); + }); + + test('every step code snippet converts to blocks without errors', async () => { + const codeSteps = deck.steps.filter(step => step.code); + expect(codeSteps.length).toBeGreaterThan(0); + for (const step of codeSteps) { + // Single-feature decks teach blocks, so the inserted Ruby has to + // land in the Code tab as blocks. + expect(step.codeType).toBe('blocks'); + const converter = new RubyToBlocksConverter(null, {version: '2'}); + const res = await converter.targetCodeToBlocks(null, step.code); + expect(converter.errors).toHaveLength(0); + expect(res).toBeTruthy(); + } + }); + }); +}); diff --git a/packages/scratch-gui/test/unit/lib/libraries/decks/tutorial-categories-refactor.test.js b/packages/scratch-gui/test/unit/lib/libraries/decks/tutorial-categories-refactor.test.js index 1513ca0875..de466082fb 100644 --- a/packages/scratch-gui/test/unit/lib/libraries/decks/tutorial-categories-refactor.test.js +++ b/packages/scratch-gui/test/unit/lib/libraries/decks/tutorial-categories-refactor.test.js @@ -27,6 +27,9 @@ import en from '../../../../../src/locales/en'; const EXPECTED_DECK_ID_ORDER = [ 'intro-getting-started', + // 単機能チュートリアル (#1179) は gettingStarted カテゴリの中で intro の直後に並ぶ + 'getting-started-costume', + 'getting-started-sprites', 'chat-1-basic-1', 'chat-1-basic-2', 'chat-1-basic-3', @@ -52,6 +55,8 @@ describe('Tutorial deck category split (issue #932)', () => { test('getting-started and ruby-basics decks are composed from their category modules', () => { expect(decks['intro-getting-started']).toBe(gettingStartedDecks['intro-getting-started']); + expect(decks['getting-started-costume']).toBe(gettingStartedDecks['getting-started-costume']); + expect(decks['getting-started-sprites']).toBe(gettingStartedDecks['getting-started-sprites']); expect(decks['ruby-basics-1-numbers']).toBe(rubyBasicsDecks['ruby-basics-1-numbers']); expect(decks['ruby-basics-2-strings']).toBe(rubyBasicsDecks['ruby-basics-2-strings']); expect(decks['ruby-basics-3-variables']).toBe(rubyBasicsDecks['ruby-basics-3-variables']);