Puppeteerを使ってScratchプログラムを自動生成してみました。
環境はこんな感じです。
Windows10でプログラミングしましたが、Macでも動きました。
>node -v
v10.15.1
>nodist -v
0.8.8
>npm -v
4.0.5
Puppeteerのセットアップはこのようにしました。
npm init -y
npm install --save puppeteer@1.11.0
下記のscratch.jsを用意してコマンドを実行すれば簡単なScratchプログラムを自動生成します。
node scratch.js
別途、settings.jsを用意しておきましょう。
settings.js
exports.username='scratchのID';
exports.password='scratchのパスワード';
exports.email='あなたのEメールアドレス';
ちなみに、Scratchのバージョンは3.0です。
バージョンが違うと動かない可能性があります。
scratch.js
const puppeteer = require('puppeteer');
const settings = require('./settings.js');
(async() => {
const options = process.env.DEBUG ? {
devtools: true,
slowMo: 100
} : {
};
const browser = await puppeteer.launch(options);
const page = await browser.newPage();
const VIEWPORT = {width: 1200, height: 800, deviceScaleFactor: 1};
await page.setViewport(VIEWPORT);
await page.setUserAgent('Puppeteer ' + settings.email);
await Promise.all([
page.waitFor('.login-item'),
page.goto('https://scratch.mit.edu', {waitUntil: "domcontentloaded"})
]);
// await page.evaluate(() => { debugger; });
// ログイン.
await page.click('.login-item');
await page.type('input[name=username]', settings.username);
await page.type('input[name=password]', settings.password);
await Promise.all([
page.waitFor('.account-nav'),
page.click('.submit-button')
]);
// 作る.
const inputProjectTitleSelector = 'input[class*="project-title-input"]';
await Promise.all([
page.waitFor(inputProjectTitleSelector),
page.goto('https://scratch.mit.edu/projects/editor', {waitUntil: "domcontentloaded"})
]);
// 作品タイトル設定.
const projectTitle = await page.$(inputProjectTitleSelector);
selectText(page, inputProjectTitleSelector);
const nowDate = new Date();
const title = 'Created by Puppeteer at ' + (nowDate.getMonth() + 1) + '/' + nowDate.getDate() + ' ' + nowDate.getHours() + ':' + nowDate.getMinutes();
await projectTitle.type(title);
// await Test1(page);
await Test2(page);
// 「旗」.
const greenFlag = await page.$('[class*="green-flag_green-flag"]')
if (greenFlag != null) {
await greenFlag.click();
await page.waitFor(11000);
}
// await page.evaluate(() => { debugger; });
// 「直ちに保存」.
await page.waitFor(3000);
const save = await page.$('[class*="save-status_save-now"]');
if (save != null) {
await save.click();
await page.waitFor(3000);
}
// サインアウト.
await page.waitFor('div[class*="account-nav_dropdown-caret"]');
await page.click('div[class*="account-nav_dropdown-caret"]');
await page.click('div[class*="menu-bar_account-info"] li[class*="menu_menu-section"]');
await page.waitFor(3000);
await browser.close();
})();
const selectText = async function (page, selector) {
await page.evaluate(selector => {
const title = document.querySelector(selector);
title.select();
}, selector);
};
const dragAndDrop = async function (page, fromX, fromY, toX, toY) {
await page.mouse.move(fromX, fromY);
await page.mouse.down();
await page.mouse.move(toX, toY, {steps: 1});
await page.mouse.up();
};
const searchBlockPalette = async function (page, dataId, yMax) {
for (let count=0; count < 1000; count++) {
let rectBlock = await page.$eval('[data-id="'+dataId+'"]', el => {
const { width, height, top: y, left: x } = el.getBoundingClientRect();
return { width, height, x, y };
});
if (0 < rectBlock.y && rectBlock.y < yMax) {
break;
}
let rectScrollbarHandle = await page.$eval('.blocklyFlyoutScrollbar .blocklyScrollbarHandle', el => {
const { width, height, top: y, left: x } = el.getBoundingClientRect();
return { width, height, x, y };
});
let clickPosX = rectScrollbarHandle.x + (rectScrollbarHandle.width / 2);
let clickPosY = rectScrollbarHandle.y;
if (rectBlock.y <= 0) {
clickPosY -= 10;
if (clickPosY < 0) {
clickPosY = 0;
}
}
else if (rectBlock.y >= yMax) {
clickPosY += (10 + rectScrollbarHandle.height);
}
console.log('click:('+clickPosX+','+clickPosY+')');
await page.mouse.move(clickPosX, clickPosY);
await page.mouse.down();
await page.mouse.up();
}
};
const selectCategory = async function (page, category) {
const categoryControl = await page.$('.scratchCategoryId-'+category);
if (categoryControl != null) {
await categoryControl.click();
await page.waitFor(1000);
}
};
const addBlock = async function (page, dataId, parentRect, nest=false) {
let rect = await page.$eval('[data-id="'+dataId+'"]', el => {
const { width, height, top: y, left: x } = el.getBoundingClientRect();
return { width, height, x, y };
});
const yMax = 500;
if (rect.y > yMax) {
await searchBlockPalette(page, dataId, yMax);
rect = await page.$eval('[data-id="'+dataId+'"]', el => {
const { width, height, top: y, left: x } = el.getBoundingClientRect();
return { width, height, x, y };
});
}
const offset_x = 20;
const offset_y = 20;
const move_x = 0;
const move_y = parentRect.height;
const nest_x = 20;
const nest_y = 30;
let fromX = rect.x + offset_x;
let fromY = rect.y + offset_y;
let toX = parentRect.x + offset_x;
let toY = parentRect.y + offset_y;
if (nest) {
toX += nest_x;
toY += nest_y;
}
else {
toX += move_x;
toY += move_y;
}
console.log('fromX:'+fromX+' fromY:'+fromY+' toX:'+toX+' toY:'+toY);
await dragAndDrop(page, fromX, fromY, toX, toY);
};
const addBlockEx = async function (page, category, dataId, blockId, parentBlockId='', nest=false) {
console.log('addBlockEx start');
// カテゴリ.
console.log('category:'+category);
await selectCategory(page, category);
// 親ブロック.
let parentRect = { width: 0, height: 0, x: 0, y: 0 };
if (parentBlockId != '') {
console.log('parentBlockId:'+parentBlockId);
parentRect = await page.$eval('[data-block-id="'+parentBlockId+'"]', el => {
const { width, height, top: y, left: x } = el.getBoundingClientRect();
return { width, height, x, y };
});
}
else {
console.log('dataId:'+dataId);
const rect = await page.$eval('[data-id="'+dataId+'"]', el => {
const { width, height, top: y, left: x } = el.getBoundingClientRect();
return { width, height, x, y };
});
parentRect = { width: 0, height: 0, x: rect.x + 400, y: rect.y + 0 };
}
// ブロック.
console.log('parentRect width:'+parentRect.width+' height:'+parentRect.height+' x:'+parentRect.x+' y:'+parentRect.y);
await addBlock(page, dataId, parentRect, nest);
console.log('evaluate');
await page.evaluate((selector, blockId) => {
console.log('selector:'+selector);
const el = document.querySelector(selector);
console.log('blockId:'+blockId);
el.setAttribute('data-block-id', blockId);
}, '.blocklySelected', blockId);
};
const Test1 = async function (page) {
// 「イベント」.
// 「フラッグが押されたとき」.
const dataBlockIdWhenFlagClicked_0 = 'dataBlockIdWhenFlagClicked_0';
await addBlockEx(page, 'events', 'event_whenflagclicked', dataBlockIdWhenFlagClicked_0);
// 「制御」.
// 「10回繰り返す」.
const dataBlockIdRepeat_0 = 'dataBlockIdRepeat_0';
await addBlockEx(page, 'control', 'control_repeat', dataBlockIdRepeat_0, dataBlockIdWhenFlagClicked_0);
// 「動き」.
// 「10歩動かす」.
const dataBlockIdMovesteps_0 = 'dataBlockIdMovesteps_0';
await addBlockEx(page, 'motion', 'motion_movesteps', dataBlockIdMovesteps_0, dataBlockIdRepeat_0, true);
// 「制御」.
// 「1秒待つ」.
const dataBlockIdWait_0 = 'dataBlockIdWait_0';
await addBlockEx(page, 'control', 'control_wait', dataBlockIdWait_0, dataBlockIdMovesteps_0);
};
// ネコあるき.
const Test2 = async function (page) {
// 「イベント」.
// 「フラッグが押されたとき」.
const dataBlockIdWhenFlagClicked_0 = 'dataBlockIdWhenFlagClicked_0';
await addBlockEx(page, 'events', 'event_whenflagclicked', dataBlockIdWhenFlagClicked_0);
// 「動き」.
// 「回転方法を左右のみにする」.
const dataBlockIdSetRotationStyle_0 = 'dataBlockIdSetRotationStyle_0';
await addBlockEx(page, 'motion', 'motion_setrotationstyle', dataBlockIdSetRotationStyle_0, dataBlockIdWhenFlagClicked_0);
// 「制御」.
// 「ずっと」.
const dataBlockIdForever_0 = 'dataBlockIdForever_0';
await addBlockEx(page, 'control', 'forever', dataBlockIdForever_0, dataBlockIdSetRotationStyle_0);
// 「動き」.
// 「10歩動かす」.
const dataBlockIdMovesteps_0 = 'dataBlockIdMovesteps_0';
await addBlockEx(page, 'motion', 'motion_movesteps', dataBlockIdMovesteps_0, dataBlockIdForever_0, true);
// 「動き」.
// 「もし端に着いたら、跳ね返る」.
const dataBlockIdIfOnEdgeBounce_0 = 'dataBlockIdIfOnEdgeBounce_0';
await addBlockEx(page, 'motion', 'motion_ifonedgebounce', dataBlockIdIfOnEdgeBounce_0, dataBlockIdMovesteps_0);
// 「見た目」.
// 「次のコスチュームにする」.
const dataBlockIdNextCostume_0 = 'dataBlockIdNextCostume_0';
await addBlockEx(page, 'looks', 'looks_nextcostume', dataBlockIdNextCostume_0, dataBlockIdMovesteps_0);
// 「動き」.
let category = 'motion';
await selectCategory(page, category);
// 「15度回す」×3.
const offset_x = 20;
const offset_y = 20;
let dataId = 'motion_turnleft';
let rect = await page.$eval('[data-id="'+dataId+'"]', el => {
const { width, height, top: y, left: x } = el.getBoundingClientRect();
return { width, height, x, y };
});
await page.mouse.move(rect.x + offset_x, rect.y + offset_y);
for (let i=0; i < 3; i++) {
await page.mouse.down();
await page.mouse.up();
}
};