Initial commit of 001code-html Scratch frontend project.

Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 15:37:45 +08:00
commit 6e0a1fbcbb
11350 changed files with 965674 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByText,
findByXpath,
getDriver,
getLogs,
loadUri,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
describe('Working with backdrops', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Adding a backdrop from the library should not switch to stage', async () => {
await loadUri(uri);
// Start on the sounds tab of sprite1 to test switching behavior
await clickText('Sounds');
// Add a backdrop without selecting the stage first to test switching
await clickXpath('//button[@aria-label="Choose a Backdrop"]');
const el = await findByXpath("//input[@placeholder='Search']");
await el.sendKeys('blue');
await clickText('Blue Sky'); // Adds the backdrop
// Make sure the sprite is still selected, and that the tab has not changed
await clickText('Meow', scope.soundsTab);
// Make sure the backdrop was actually added by going to the backdrops tab
await clickXpath('//span[text()="Stage"]');
await clickText('Backdrops');
await clickText('Blue Sky', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding backdrop via paint should switch to stage', async () => {
await loadUri(uri);
const buttonXpath = '//button[@aria-label="Choose a Backdrop"]';
const paintXpath = `${buttonXpath}/following-sibling::div//button[@aria-label="Paint"]`;
const el = await findByXpath(buttonXpath);
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
await clickXpath(paintXpath);
// Stage should become selected and costume tab activated
await findByText('backdrop2', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding backdrop via surprise should not switch to stage', async () => {
await loadUri(uri);
// Start on the sounds tab of sprite1 to test switching behavior
await clickText('Sounds');
const buttonXpath = '//button[@aria-label="Choose a Backdrop"]';
const surpriseXpath = `${buttonXpath}/following-sibling::div//button[@aria-label="Surprise"]`;
const el = await findByXpath(buttonXpath);
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
await clickXpath(surpriseXpath);
// Make sure the sprite is still selected, and that the tab has not changed
await clickText('Meow', scope.soundsTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding multiple backdrops from file should switch to stage', async () => {
const files = [
path.resolve(__dirname, '../fixtures/gh-3582-png.png'),
path.resolve(__dirname, '../fixtures/100-100.svg')
];
await loadUri(uri);
const buttonXpath = '//button[@aria-label="Choose a Backdrop"]';
const fileXpath = `${buttonXpath}/following-sibling::div//input[@type="file"]`;
const el = await findByXpath(buttonXpath);
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath(fileXpath);
await input.sendKeys(files.join('\n'));
// Should have been switched to stage/costume tab already
await findByText('gh-3582-png', scope.costumesTab);
await findByText('100-100', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
});

View File

@@ -0,0 +1,44 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
getDriver,
getLogs,
loadUri
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
describe('Working with the how-to library', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Backpack is "Coming Soon" without backpack host param', async () => {
await loadUri(uri);
// Check that the backpack header is visible and wrapped in a coming soon tooltip
await clickText('Backpack', '*[@data-for="backpack-tooltip"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Backpack can be expanded with backpack host param', async () => {
await loadUri(`${uri}?backpack_host=https://backpack.scratch.mit.edu`);
// Try activating the backpack from the costumes tab to make sure it isn't pushed off
await clickText('Costumes');
// Check that the backpack header is visible and wrapped in a coming soon tooltip
await clickText('Backpack'); // Not wrapped in tooltip
await clickText('Backpack is empty'); // Make sure it can expand, is empty
const logs = await getLogs();
await expect(logs).toEqual([]);
});
});

View File

@@ -0,0 +1,339 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickBlocksCategory,
clickButton,
clickXpath,
findByText,
findByXpath,
textExists,
getDriver,
getLogs,
Key,
loadUri,
rightClickText,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
'[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]';
describe('Working with the blocks', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Blocks report when clicked in the toolbox', async () => {
await loadUri(uri);
await clickText('Code');
await clickBlocksCategory('Operators');
await clickText('join', scope.blocksTab); // Click "join <hello> <world>" block
await findByText('apple banana', scope.reportedValue); // Tooltip with result
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Switching sprites updates the block menus', async () => {
await loadUri(uri);
await clickBlocksCategory('Sound');
// "Meow" sound block should be visible
await findByText('Meow', scope.blocksTab);
await clickText('Backdrops'); // Switch to the backdrop
// Now "pop" sound block should be visible and motion blocks hidden
await findByText('pop', scope.blocksTab);
await clickBlocksCategory('Motion');
await findByText('Stage selected: no motion blocks');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Creating variables', async () => {
await loadUri(uri);
await clickText('Code');
await clickBlocksCategory('Variables');
// Expect a default variable "my variable" to be visible
await clickText('my\u00A0variable', scope.blocksTab);
await findByText('0', scope.reportedValue);
await clickText('Make a Variable');
let el = await findByXpath("//input[@name='New variable name:']");
await el.sendKeys('score');
await clickButton('OK');
await clickText('Make a Variable');
el = await findByXpath("//input[@name='New variable name:']");
await el.sendKeys('second variable');
await clickButton('OK');
// Make sure reporting works on a new variable
await clickBlocksCategory('Variables');
await clickText('score', scope.blocksTab);
await findByText('0', scope.reportedValue); // Tooltip with result
// And there should be a monitor visible
await rightClickText('score', scope.monitors);
await clickText('slider');
await findByXpath("//input[@step='1']");
// Changing the slider to a decimal should make it have a step size of 0.01
await rightClickText('score', scope.monitors);
await clickText('change slider range');
el = await findByXpath("//input[@name='Maximum value']");
await el.sendKeys('.1');
await clickButton('OK');
await findByXpath("//input[@step='0.01'][@max='100.1']");
// Hiding the monitor via context menu should work
await rightClickText('score', scope.monitors);
await clickText('hide', scope.contextMenu);
await driver.sleep(100);
const monitorExists = await textExists('score', scope.monitors);
await expect(monitorExists).toBeFalsy();
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Creating a list', async () => {
await loadUri(uri);
await clickText('Code');
await clickBlocksCategory('Variables');
await clickText('Make a List');
let el = await findByXpath("//input[@name='New list name:']");
await el.sendKeys('list1');
await clickButton('OK');
// Click the "add <thing> to list" block 3 times
await clickText('add', scope.blocksTab);
await clickText('add', scope.blocksTab);
await clickText('add', scope.blocksTab);
await clickText('list1', scope.blocksTab);
await findByText('thing thing thing', scope.reportedValue); // Tooltip with result
// Interact with the monitor, adding an item
await findByText('list1', scope.monitors); // Just to be sure it is there
await clickText('+', scope.monitors);
el = await findByXpath(`//body//${scope.monitors}//input`);
await el.sendKeys('thing2');
await el.click(); // Regression for "clicking active input erases value" bug.
await clickText('list1', scope.monitors); // Blur the input to submit
// Check that the list value has been propagated.
await clickText('list1', scope.blocksTab);
await findByText('thing thing thing thing2', scope.reportedValue); // Tooltip with result
// Hiding the monitor via context menu should work
await rightClickText('list1', scope.monitors);
await clickText('hide', scope.contextMenu);
await driver.sleep(100);
const monitorExists = await textExists('list1', scope.monitors);
await expect(monitorExists).toBeFalsy();
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Custom procedures', async () => {
await loadUri(uri);
await clickBlocksCategory('My Blocks');
await clickText('Make a Block');
// Click on the "add an input" buttons
await clickText('number or text', scope.modal);
await clickText('boolean', scope.modal);
await clickText('Add a label', scope.modal);
await clickText('OK', scope.modal);
// Make sure a "define" block has been added to the workspace
await findByText('define', scope.blocksTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding an extension', async () => {
await loadUri(uri);
await clickXpath('//button[@title="Add Extension"]');
await clickText('Pen');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
// Make sure toolbox has been scrolled to the pen extension
await findByText('stamp', scope.blocksTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Record option from sound block menu opens sound recorder', async () => {
await loadUri(uri);
await clickText('Code');
await clickBlocksCategory('Sound');
await clickText('Meow', scope.blocksTab); // Click "play sound <Meow> until done" block
await clickText('record'); // Click "record..." option in the block's sound menu
// Access has been force denied, so close the alert that comes up
await driver.sleep(1000); // getUserMedia requests are very slow to fail for some reason
await driver.switchTo().alert()
.accept();
await findByText('Record Sound'); // Sound recorder is open
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Renaming costume changes the default costume name in the toolbox', async () => {
await loadUri(uri);
// Rename the costume
await clickText('Costumes');
await clickText('costume2', scope.costumesTab);
const el = await findByXpath("//input[@value='costume2']");
await el.sendKeys('newname');
await el.sendKeys(Key.ENTER);
// wait until the updated costume appears in costume item list panel
await findByXpath("//div[contains(@class,'sprite-selector-item_is-selected_')]" +
"//div[contains(text(), 'newname')]");
// Make sure it is updated in the block menu
await clickText('Code');
await clickBlocksCategory('Looks');
await clickText('newname', scope.blocksTab);
});
test('Renaming costume with a special character should not break toolbox', async () => {
await loadUri(uri);
// Rename the costume
await clickText('Costumes');
await clickText('costume2', scope.costumesTab);
const el = await findByXpath("//input[@value='costume2']");
await el.sendKeys('<NewCostume>');
await el.sendKeys(Key.ENTER);
// wait until the updated costume appears in costume item list panel
await findByXpath("//div[contains(@class,'sprite-selector-item_is-selected_')]" +
"//div[contains(text(), '<NewCostume>')]");
// Make sure it is updated in the block menu
await clickText('Code');
await clickBlocksCategory('Looks');
await clickText('<NewCostume>', scope.blocksTab);
await clickBlocksCategory('Sound');
});
test('Adding costumes DOES update the default costume name in the toolbox', async () => {
await loadUri(uri);
// By default, costume2 is in the costume tab
await clickBlocksCategory('Looks');
await clickText('costume2', scope.blocksTab);
// Also check that adding a new costume does update the list
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
await clickXpath('//button[@aria-label="Paint"]');
// wait until the new costume appears in costume item list panel
await findByXpath("//div[contains(@class,'sprite-selector-item_is-selected_')]" +
"//div[contains(text(), 'costume3')]");
await clickText('costume3', scope.costumesTab);
// Check that the menu has been updated
await clickText('Code');
await clickText('costume3', scope.blocksTab);
});
// Skipped because it was flakey on travis, but seems to run locally ok
test('Adding a sound DOES update the default sound name in the toolbox', async () => {
await loadUri(uri);
await clickText('Sounds');
await clickXpath('//button[@aria-label="Choose a Sound"]');
await clickText('A Bass', scope.modal); // Should close the modal
// wait until the selected sound appears in sounds item list panel
await findByXpath("//div[contains(@class,'sprite-selector-item_is-selected_')]" +
"//div[contains(text(), 'A Bass')]");
await clickText('Code');
await clickBlocksCategory('Sound');
await clickText('A\u00A0Bass', scope.blocksTab); // Need &nbsp; for block text
});
// Regression test for switching between editor/player causing toolbox to stop updating
test('"See inside" after being on project page re-initializing variables', async () => {
const playerUri = path.resolve(__dirname, '../../build/player.html');
await loadUri(playerUri);
await clickText('See inside');
await clickBlocksCategory('Variables');
await clickText('my\u00A0variable');
await clickText('See Project Page');
await clickText('See inside');
await clickBlocksCategory('Variables');
await clickText('my\u00A0variable');
});
// Regression test for switching editor tabs causing toolbox to stop updating
test('Creating variables after adding extensions updates the toolbox', async () => {
await loadUri(uri);
await clickText('Costumes');
await clickText('Code');
await clickBlocksCategory('Variables');
await clickText('Make a List');
const el = await findByXpath("//input[@name='New list name:']");
await el.sendKeys('list1');
await clickButton('OK');
await clickText('list1', scope.blocksTab);
});
test('Use variable blocks after switching languages', async () => {
const myVariable = 'my\u00A0variable';
const changeVariableByScope = "*[@data-id='data_changevariableby']";
await loadUri(uri);
await clickText('Code');
await clickBlocksCategory('Variables');
// change "my variable" by 1
await clickText('change', changeVariableByScope);
// check reported value 1
await clickText(myVariable, scope.blocksTab);
await findByText('1', scope.reportedValue);
// change language
await clickXpath(SETTINGS_MENU_XPATH);
await clickText('Language', scope.menuBar);
await clickText('Deutsch');
await clickText('Skripte');
await clickBlocksCategory('Variablen');
// make sure "my variable" is still 1
await clickText(myVariable);
await findByText('1', scope.reportedValue);
// change step from 1 to 10
await clickText('1', changeVariableByScope);
await driver.actions()
.sendKeys('10')
.perform();
// change "my variable" by 10
await clickText('ändere', changeVariableByScope);
// check it is turned up to 11
await clickText(myVariable);
await findByText('11', scope.reportedValue);
});
});

View File

@@ -0,0 +1,67 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByText,
getDriver,
getLogs,
loadUri
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
// The tests below require Scratch Link to be unavailable, so we can trigger
// an error modal. To make sure this is always true, we came up with the idea of
// injecting javascript that overwrites the global Websocket object with one that
// attempts to connect to a fake socket address.
const websocketFakeoutJs = `var RealWebSocket = WebSocket;
WebSocket = function () {
return new RealWebSocket("wss://fake.fake");
}`;
describe('Hardware extension connection modal', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Message saying Scratch Link is unavailable (BLE)', async () => {
await driver.quit();
driver = getDriver();
await loadUri(uri);
await driver.executeScript(websocketFakeoutJs);
await clickXpath('//button[@title="Add Extension"]');
await clickText('micro:bit');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for modal to open
findByText('Scratch Link'); // Scratch Link is mentioned in the error modal
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Message saying Scratch Link is unavailable (BT)', async () => {
await loadUri(uri);
await driver.executeScript(websocketFakeoutJs);
await clickXpath('//button[@title="Add Extension"]');
await clickText('EV3');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for modal to open
findByText('Scratch Link'); // Scratch Link is mentioned in the error modal
const logs = await getLogs();
await expect(logs).toEqual([]);
});
});

View File

@@ -0,0 +1,262 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByText,
findByXpath,
getDriver,
getLogs,
loadUri,
rightClickText,
scope
} = new SeleniumHelper();
// The costumes library is slow to load. Increase the timeout for these tests.
jest.setTimeout(60_000);
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
describe('Working with costumes', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Adding a costume through the library', async () => {
await loadUri(uri);
await driver.sleep(500);
await clickText('Costumes');
await clickXpath('//button[@aria-label="Choose a Costume"]');
const el = await findByXpath("//input[@placeholder='Search']");
await el.sendKeys('abb');
await clickText('Abby-a'); // Should close the modal, then click the costumes in the selector
await findByXpath("//input[@value='Abby-a']"); // Should show editor for new costume
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a costume by surprise button', async () => {
await loadUri(uri);
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
await clickXpath('//button[@aria-label="Surprise"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a costume by paint button', async () => {
await loadUri(uri);
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
await clickXpath('//button[@aria-label="Paint"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Duplicating a costume', async () => {
await loadUri(uri);
await clickText('Costumes');
await rightClickText('costume1', scope.costumesTab);
await clickText('duplicate', scope.costumesTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for duplication to finish
// Make sure the duplicated costume is named correctly.
await clickText('costume3', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Converting bitmap/vector in paint editor', async () => {
await loadUri(uri);
await clickText('Costumes');
// Convert the first costume to bitmap.
await clickText('costume1', scope.costumesTab);
await clickText('Convert to Bitmap', scope.costumesTab);
// Make sure mode switches back to vector for vector costume.
await clickText('costume2', scope.costumesTab);
await clickText('Convert to Bitmap', scope.costumesTab);
// Make sure bitmap is saved by switching back and converting to vector.
await clickText('Sounds');
await clickText('Costumes');
await clickText('Convert to Vector', scope.costumesTab); // costume2
await clickText('costume1', scope.costumesTab);
await clickText('Convert to Vector', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Undo/redo in the paint editor', async () => {
await loadUri(uri);
await clickText('Costumes');
await clickText('costume1', scope.costumesTab);
await clickText('Convert to Bitmap', scope.costumesTab);
await clickXpath('//img[@alt="Undo"]');
await clickText('Convert to Bitmap', scope.costumesTab);
await clickXpath('//img[@alt="Undo"]');
await clickXpath('//img[@alt="Redo"]');
await clickText('Convert to Vector', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding an svg from file', async () => {
await loadUri(uri);
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/100-100.svg'));
await clickText('100-100', scope.costumesTab); // Name from filename
await clickText('100 x 100', scope.costumesTab); // Size is right
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a png from file (gh-3582)', async () => {
await loadUri(uri);
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/gh-3582-png.png'));
await clickText('gh-3582-png', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a bmp from file', async () => {
await loadUri(uri);
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/bmpfile.bmp'));
await clickText('bmpfile', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding several costumes with a gif', async () => {
await loadUri(uri);
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/paddleball.gif'));
await findByText('paddleball', scope.costumesTab);
await findByText('paddleball2', scope.costumesTab);
await findByText('paddleball3', scope.costumesTab);
await findByText('paddleball4', scope.costumesTab);
await findByText('paddleball5', scope.costumesTab);
await findByText('paddleball6', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a letter costume through the Letters filter in the library', async () => {
await loadUri(uri);
await driver.manage()
.window()
.setSize(1244, 768); // Letters filter not visible at 1024 width
await clickText('Costumes');
await clickXpath('//button[@aria-label="Choose a Costume"]');
await clickText('Letters');
await clickText('Block-a', scope.modal); // Closes modal
await rightClickText('Block-a', scope.costumesTab); // Make sure it is there
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Costumes animate on mouseover', async () => {
await loadUri(uri);
await clickXpath('//button[@aria-label="Choose a Sprite"]');
const searchElement = await findByXpath("//input[@placeholder='Search']");
await searchElement.sendKeys('abb');
const abbyElement = await findByXpath('//*[span[text()="Abby"]]');
driver.actions()
.mouseMove(abbyElement)
.perform();
// wait for one of Abby's alternate costumes to appear
await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/45de34b47a2ce22f6f5d28bb35a44ff5.svg/get/"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding multiple costumes at the same time', async () => {
const files = [
path.resolve(__dirname, '../fixtures/gh-3582-png.png'),
path.resolve(__dirname, '../fixtures/100-100.svg')
];
await loadUri(uri);
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(files.join('\n'));
await findByText('gh-3582-png', scope.costumesTab);
await findByText('100-100', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Load an invalid svg from scratch3 as costume', async () => { // eslint-disable-line no-disabled-tests
await loadUri(uri);
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-from-scratch3.svg'));
const costumeTile = await findByText('corrupt-from-scratch3', scope.costumesTab); // Name from filename
const tileVisible = await costumeTile.isDisplayed();
await expect(tileVisible).toBe(true);
});
test('Load an invalid svg from scratch2 as costume', async () => { // eslint-disable-line no-disabled-tests
await loadUri(uri);
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/scratch2-corrupted.svg'));
const costumeTile = await findByText('scratch2-corrupted', scope.costumesTab); // Name from filename
const tileVisible = await costumeTile.isDisplayed();
await expect(tileVisible).toBe(true);
});
});

View File

@@ -0,0 +1,100 @@
/* globals Promise */
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickButton,
clickText,
clickXpath,
findByXpath,
getDriver,
getLogs,
loadUri
} = new SeleniumHelper();
let driver;
describe('player example', () => {
const uri = path.resolve(__dirname, '../../build/player.html');
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test.skip('Player: load a project by ID', async () => {
const projectId = '96708228';
await loadUri(`${uri}#${projectId}`);
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Stop"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
const projectRequests = await driver.manage().logs()
.get('performance')
.then(pLogs => pLogs.map(log => JSON.parse(log.message).message)
.filter(m => m.method === 'Network.requestWillBeSent')
.map(m => m.params.request.url)
.filter(url => url === 'https://projects.scratch.mit.edu/96708228')
);
await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/96708228']);
});
});
describe('blocks example', () => {
const uri = path.resolve(__dirname, '../../build/blocks-only.html');
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test.skip('Blocks: load a project by ID', async () => {
const projectId = '96708228';
await loadUri(`${uri}#${projectId}`);
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Stop"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
const projectRequests = await driver.manage().logs()
.get('performance')
.then(pLogs => pLogs.map(log => JSON.parse(log.message).message)
.filter(m => m.method === 'Network.requestWillBeSent')
.map(m => m.params.request.url)
.filter(url => url === 'https://projects.scratch.mit.edu/96708228')
);
await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/96708228']);
});
// skipping per https://github.com/LLK/scratch-gui/issues/4902 until we have better approach
test.skip('Change categories', async () => {
await loadUri(`${uri}`);
await clickText('Looks');
await clickText('Sound');
await clickText('Events');
await clickText('Control');
await clickText('Sensing');
await clickText('Operators');
await clickText('Variables');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await clickText('Make a Variable');
let el = await findByXpath("//input[@name='New variable name:']");
await el.sendKeys('score');
await clickButton('OK');
await clickText('Make a Variable');
el = await findByXpath("//input[@name='New variable name:']");
await el.sendKeys('second variable');
await clickButton('OK');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
});

View File

@@ -0,0 +1,38 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByXpath,
getDriver,
getLogs,
loadUri
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
describe('Working with the how-to library', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Choosing a how-to', async () => {
await loadUri(uri);
await clickText('Costumes');
await clickXpath('//*[@aria-label="Tutorials"]');
await clickText('Getting Started'); // Modal should close
// Make sure YouTube video on first card appears
await findByXpath('//div[contains(@class, "step-video")]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// @todo navigating cards, etc.
});

View File

@@ -0,0 +1,109 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByText,
findByXpath,
getDriver,
getLogs,
loadUri,
rightClickText,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
'[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]';
describe('Localization', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Switching languages', async () => {
await loadUri(uri);
// Add a sprite to make sure it stays when switching languages
await clickXpath('//button[@aria-label="Choose a Sprite"]');
await clickText('Apple', scope.modal); // Closes modal
await clickXpath(SETTINGS_MENU_XPATH);
await clickText('Language', scope.menuBar);
await clickText('Deutsch');
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks refresh
// Make sure the blocks are translating
await clickText('Fühlen'); // Sensing category in German
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks to scroll
await clickText('Antwort'); // Find the "answer" block in German
// Change to the costumes tab to confirm other parts of the GUI are translating
await clickText('Kostüme');
// After switching languages, make sure Apple sprite still exists
await rightClickText('Apple', scope.spriteTile); // Make sure it is there
// Remounting re-attaches the beforeunload callback. Make sure to remove it
driver.executeScript('window.onbeforeunload = undefined;');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// Regression test for #4476, blocks in wrong language when loaded with locale
test('Loading with locale shows correct blocks', async () => {
await loadUri(`${uri}?locale=de`);
await clickText('Fühlen'); // Sensing category in German
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks to scroll
await clickText('Antwort'); // Find the "answer" block in German
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// test for #5445
test('Loading with locale shows correct translation for string length block parameter', async () => {
await loadUri(`${uri}?locale=ja`);
await clickText('演算'); // Operators category in Japanese
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks to scroll
await clickText('の長さ', scope.blocksTab); // Click "length <apple>" block
await findByText('3', scope.reportedValue); // Tooltip with result
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// Regression test for ENA-142, monitor can lag behind language selection
test('Monitor labels update on locale change', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/monitor-variable.sb3'));
// Monitors are present
await findByText('username', scope.monitors);
await findByText('language', scope.monitors);
// Change locale to ja
await clickXpath(SETTINGS_MENU_XPATH);
await clickText('Language', scope.menuBar);
await clickText('日本語');
// Monitor labels updated
await findByText('ユーザー名', scope.monitors);
await findByText('言語', scope.monitors);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
});

View File

@@ -0,0 +1,172 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByText,
findByXpath,
getDriver,
loadUri,
rightClickText,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
'[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]';
describe('Menu bar settings', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('File->New should be enabled', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await findByXpath('//*[li[span[text()="New"]] and not(@data-tip="tooltip")]');
});
test('File->Load should be enabled', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await findByXpath('//*[li[text()="Load from your computer"] and not(@data-tip="tooltip")]');
});
test('File->Save should be enabled', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await findByXpath('//*[li[span[text()="Save to your computer"]] and not(@data-tip="tooltip")]');
});
test('Share button should NOT be enabled', async () => {
await loadUri(uri);
await findByXpath('//div[span[div[span[text()="Share"]]] and @data-tip="tooltip"]');
});
test('Logo should be clickable', async () => {
await loadUri(uri);
await clickXpath('//img[@alt="Scratch"]');
const currentUrl = await driver.getCurrentUrl();
await expect(currentUrl).toEqual('https://scratch.mit.edu/');
});
test('(GH#4064) Project name should be editable', async () => {
await loadUri(uri);
const el = await findByXpath('//input[@value="Scratch Project"]');
await el.sendKeys(' - Personalized');
await clickText('Costumes'); // just to blur the input
await clickXpath('//input[@value="Scratch Project - Personalized"]');
});
test('User is not warned before uploading project file over a fresh project', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
// No replace alert since no changes were made
await findByText('project1-sprite');
});
test('User is warned before uploading project file over an edited project', async () => {
await loadUri(uri);
// Change the project by deleting a sprite
await rightClickText('Sprite1', scope.spriteTile);
await clickText('delete', scope.spriteTile);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
await driver.switchTo().alert()
.accept();
await findByText('project1-sprite');
});
test('Theme picker shows themes', async () => {
await loadUri(uri);
await clickXpath(SETTINGS_MENU_XPATH);
await clickText('Color Mode', scope.menuBar);
expect(await (await findByText('Original', scope.menuBar)).isDisplayed()).toBe(true);
expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(true);
});
test('Theme picker switches to high contrast', async () => {
await loadUri(uri);
await clickXpath(SETTINGS_MENU_XPATH);
await clickText('Color Mode', scope.menuBar);
await clickText('High Contrast', scope.menuBar);
// There is a tiny delay for the color theme to be applied to the categories.
await driver.wait(async () => {
const motionCategoryDiv = await findByXpath(
'//div[contains(@class, "scratchCategoryMenuItem") and ' +
'contains(@class, "scratchCategoryId-motion")]/*[1]');
const color = await motionCategoryDiv.getCssValue('background-color');
// Documentation for getCssValue says it depends on how the browser
// returns the value. Locally I am seeing 'rgba(128, 181, 255, 1)',
// but this is a bit flexible just in case.
return /128,\s?181,\s?255/.test(color) || color.includes('80B5FF');
}, 5000, 'Motion category color does not match high contrast theme');
});
test('Settings menu switches between submenus', async () => {
await loadUri(uri);
await clickXpath(SETTINGS_MENU_XPATH);
// Language and theme options not visible yet
expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(false);
expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(false);
await clickText('Color Mode', scope.menuBar);
// Only theme options visible
expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(true);
expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(false);
await clickText('Language', scope.menuBar);
// Only language options visible
expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(false);
expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(true);
});
test('Menu labels hidden when width is equal to 1024', async () => {
await loadUri(uri);
await driver.manage()
.window()
.setSize(1024, 768);
const collapsibleMenus = ['Settings', 'File', 'Edit', 'Tutorials'];
for (const menu of collapsibleMenus) {
const settingsMenu = await findByText(menu, scope.menuBar);
expect(await settingsMenu.isDisplayed()).toBe(false);
}
});
test('Menu labels shown when width is greater than 1024', async () => {
await loadUri(uri);
await driver.manage()
.window()
.setSize(1200, 768);
const collapsibleMenus = ['Settings', 'File', 'Edit', 'Tutorials'];
for (const menu of collapsibleMenus) {
const settingsMenu = await findByText(menu, scope.menuBar);
expect(await settingsMenu.isDisplayed()).toBe(true);
}
});
});

View File

@@ -0,0 +1,117 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByText,
findByXpath,
getDriver,
getLogs,
loadUri,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
describe('Loading scratch gui', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
describe('Loading projects by ID', () => {
test('Nonexistent projects show error screen', async () => {
await loadUri(`${uri}#999999999999999999999`);
await clickText('Oops! Something went wrong.');
});
// skipping because it relies on network speed, and tests a method
// of loading projects that we are not actively using anymore
test.skip('Load a project by ID directly through url', async () => {
await driver.quit(); // Reset driver to test hitting # url directly
driver = getDriver();
const projectId = '96708228';
await loadUri(`${uri}#${projectId}`);
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Stop"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// skipping because it relies on network speed, and tests a method
// of loading projects that we are not actively using anymore
test.skip('Load a project by ID (fullscreen)', async () => {
await driver.quit(); // Reset driver to test hitting # url directly
driver = getDriver();
const prevSize = driver.manage()
.window()
.getSize();
await new Promise(resolve => setTimeout(resolve, 2000));
driver.manage()
.window()
.setSize(1920, 1080);
const projectId = '96708228';
await loadUri(`${uri}#${projectId}`);
await clickXpath('//img[@title="Full Screen Control"]');
await new Promise(resolve => setTimeout(resolve, 500));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 1000));
await clickXpath('//img[@title="Stop"]');
prevSize.then(value => {
driver.manage()
.window()
.setSize(value.width, value.height);
});
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// skipping because this test fails frequently on CI; might need "wait(until.elementLocated" or similar
// error message is "stale element reference: element is not attached to the page document"
test.skip('Creating new project resets active tab to Code tab', async () => {
await loadUri(uri);
await findByXpath('//*[span[text()="Costumes"]]');
await clickText('Costumes');
await clickXpath(FILE_MENU_XPATH);
await clickXpath('//li[span[text()="New"]]');
await findByXpath('//div[@class="scratchCategoryMenu"]');
await clickText('Operators', scope.blocksTab);
});
test('Not logged in->made no changes to project->create new project should not show alert', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickXpath('//li[span[text()="New"]]');
await findByXpath('//*[div[@class="scratchCategoryMenu"]]');
await clickText('Operators', scope.blocksTab);
});
test.skip('Not logged in->made a change to project->create new project should show alert', async () => {
await loadUri(uri);
await clickText('Sounds');
await clickXpath('//button[@aria-label="Choose a Sound"]');
await clickText('A Bass', scope.modal); // Should close the modal
await findByText('1.28'); // length of A Bass sound
await clickXpath(FILE_MENU_XPATH);
await clickXpath('//li[span[text()="New"]]');
driver.switchTo()
.alert()
.accept();
await findByXpath('//*[div[@class="scratchCategoryMenu"]]');
await clickText('Operators', scope.blocksTab);
});
});
});

View File

@@ -0,0 +1,45 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByXpath,
getDriver,
Key,
loadUri
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
describe('Project state', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('File->New resets project title', async () => {
const defaultProjectTitle = 'Scratch Project';
await loadUri(uri);
const inputEl = await findByXpath(`//input[@value="${defaultProjectTitle}"]`);
for (let i = 0; i < defaultProjectTitle.length; i++) {
inputEl.sendKeys(Key.BACK_SPACE);
}
inputEl.sendKeys('Changed title of project');
await clickText('Costumes'); // just to blur the input
// verify that project title has changed
await clickXpath('//input[@value="Changed title of project"]');
await clickXpath(FILE_MENU_XPATH);
await clickXpath('//li[span[text()="New"]]');
// project title should be default again
await clickXpath(`//input[@value="${defaultProjectTitle}"]`);
});
});

View File

@@ -0,0 +1,114 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByText,
findByXpath,
getDriver,
loadUri
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
describe('Loading scratch gui', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Loading project file from computer succeeds, without opening failure alert', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
await findByText('project1-sprite');
// this test will fail if an alert appears, e.g. in SBFileUploaderHOC's onload() function
});
test('Loading project file from computer gives project the filename from file', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
await findByText('project1-sprite');
await clickXpath('//input[@value="project1"]');
});
test('Load sb3 project with a missing svg costume', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-sprite-svg.sb3'));
const spriteTile = await findByText('Blue Square Guy');
const tileVisible = await spriteTile.isDisplayed();
expect(tileVisible).toBe(true);
});
test('Load sb3 project with an invalid svg costume', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-svg.sb3'));
const spriteTile = await findByText('Blue Square Guy');
const tileVisible = await spriteTile.isDisplayed();
expect(tileVisible).toBe(true);
});
test('Load sb2 project with a missing svg costume', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-svg.sb2'));
const spriteTile = await findByText('Blue Guy');
const tileVisible = await spriteTile.isDisplayed();
expect(tileVisible).toBe(true);
});
test('Load sb2 project with an invalid svg costume', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-svg.sb2'));
const spriteTile = await findByText('Blue Guy');
const tileVisible = await spriteTile.isDisplayed();
expect(tileVisible).toBe(true);
});
test('Load sb3 project with a missing bmp costume', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-bmp.sb3'));
const spriteTile = await findByText('green-bmp-guy');
const tileVisible = await spriteTile.isDisplayed();
expect(tileVisible).toBe(true);
});
test('Load sb3 project with an invalid bmp costume', async () => {
await loadUri(uri);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-bmp.sb3'));
const spriteTile = await findByText('green-bmp-guy');
const tileVisible = await spriteTile.isDisplayed();
expect(tileVisible).toBe(true);
});
});

View File

@@ -0,0 +1,189 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
import {Key} from 'selenium-webdriver';
const {
clickText,
clickXpath,
findByText,
findByXpath,
getDriver,
getLogs,
loadUri,
rightClickText,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
describe('Working with sounds', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Adding a sound through the library', async () => {
await loadUri(uri);
await clickText('Sounds');
// Delete the sound
await rightClickText('Meow', scope.soundsTab);
await driver.sleep(500); // Wait a moment for context menu; only needed for local testing
await clickText('delete', scope.soundsTab);
// Add it back
await clickXpath('//button[@aria-label="Choose a Sound"]');
let el = await findByXpath("//input[@placeholder='Search']");
await el.sendKeys('meow');
await clickText('Meow', scope.modal); // Should close the modal
// Add a new sound
await clickXpath('//button[@aria-label="Choose a Sound"]');
el = await findByXpath("//input[@placeholder='Search']");
await el.sendKeys('chom');
await clickText('Chomp'); // Should close the modal, then click the sounds in the selector
await findByXpath("//input[@value='Chomp']"); // Should show editor for new sound
await clickXpath('//button[@title="Play"]');
await clickText('Louder');
await clickText('Softer');
await clickText('Faster');
await clickText('Slower');
await clickText('Robot');
await clickText('Reverse');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a sound by surprise button', async () => {
await loadUri(uri);
await clickText('Sounds');
const el = await findByXpath('//button[@aria-label="Choose a Sound"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
await clickXpath('//button[@aria-label="Surprise"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Duplicating a sound', async () => {
await loadUri(uri);
await clickText('Sounds');
await rightClickText('Meow', scope.soundsTab);
await clickText('duplicate', scope.soundsTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error
// Make sure the duplicated sound is named correctly.
await clickText('Meow2', scope.soundsTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// Regression test for gui issue #1320
test('Switching sprites with different numbers of sounds', async () => {
await loadUri(uri);
// Add a sound so this sprite has 2 sounds.
await clickText('Sounds');
await clickXpath('//button[@aria-label="Choose a Sound"]');
await clickText('A Bass'); // Closes the modal
// Now add a sprite with only one sound.
await clickXpath('//button[@aria-label="Choose a Sprite"]');
await clickText('Abby'); // Doing this used to crash the editor.
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error
// Make sure the 'Oops' screen is not visible
const content = await driver.getPageSource();
expect(content.indexOf('Oops')).toEqual(-1);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding multiple sounds at the same time', async () => {
const files = [
path.resolve(__dirname, '../fixtures/movie.wav'),
path.resolve(__dirname, '../fixtures/sneaker.wav')
];
await loadUri(uri);
await clickText('Sounds');
const el = await findByXpath('//button[@aria-label="Choose a Sound"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(files.join('\n'));
await findByText('movie', scope.soundsTab);
await findByText('sneaker', scope.soundsTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Copy to new button adds a new sound', async () => {
await loadUri(uri);
await clickText('Sounds');
await clickText('Copy to New', scope.soundsTab);
await clickText('Meow2', scope.soundsTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Copy and pasting within a sound changes its duration', async () => {
await loadUri(uri);
await clickText('Sounds');
await findByText('0.85', scope.soundsTab); // Original meow sound duration
await clickText('Copy', scope.soundsTab);
await clickText('Paste', scope.soundsTab);
await findByText('1.70', scope.soundsTab); // Sound has doubled in duration
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Can copy a sound from a sprite and paste into a sound on the stage', async () => {
await loadUri(uri);
await clickText('Sounds');
await clickText('Copy', scope.soundsTab); // Copy the meow sound
await clickXpath('//span[text()="Stage"]');
await findByText('0.02', scope.soundsTab); // Original pop sound duration
await clickText('Paste', scope.soundsTab);
await findByText('0.87', scope.soundsTab); // Duration of pop + meow sound
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Keyboard shortcuts', async () => {
const cmdCtrl = process.platform.includes('darwin') ? Key.COMMAND : Key.CONTROL;
await loadUri(uri);
await clickText('Sounds');
const el = await findByXpath('//button[@aria-label="Choose a Sound"]');
await el.sendKeys(Key.chord(cmdCtrl, 'a')); // Select all
await findByText('0.85', scope.soundsTab); // Meow sound duration
await el.sendKeys(Key.DELETE);
await findByText('0.00', scope.soundsTab); // Sound is now empty
await el.sendKeys(Key.chord(cmdCtrl, 'z')); // undo
await findByText('0.85', scope.soundsTab); // Meow sound is back
await el.sendKeys(Key.chord(cmdCtrl, Key.SHIFT, 'z')); // redo
await findByText('0.00', scope.soundsTab); // Sound is empty again
const logs = await getLogs();
await expect(logs).toEqual([]);
});
});

View File

@@ -0,0 +1,299 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
import {StaleElementReferenceError} from 'selenium-webdriver/lib/error';
import until from 'selenium-webdriver/lib/until';
const {
clickText,
clickXpath,
elementIsVisible,
findByText,
findByXpath,
getDriver,
getLogs,
loadUri,
rightClickText,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
describe('Working with sprites', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Adding a sprite through the library', async () => {
await loadUri(uri);
await clickText('Costumes');
await clickXpath('//button[@aria-label="Choose a Sprite"]');
await clickText('Apple', scope.modal); // Closes modal
await rightClickText('Apple', scope.spriteTile); // Make sure it is there
await clickText('Motion'); // Make sure we are back to the code tab
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a sprite by surprise button', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
await clickXpath('//button[@aria-label="Surprise"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a sprite by paint button', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
await clickXpath('//button[@aria-label="Paint"]');
await findByText('Convert to Bitmap'); // Make sure we are on the paint editor
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Deleting only sprite does not crash', async () => {
await loadUri(uri);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await rightClickText('Sprite1', scope.spriteTile);
await clickText('delete', scope.spriteTile);
// Confirm that the stage has been switched to
await findByText('Stage selected: no motion blocks');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Deleting by x button on sprite tile', async () => {
await loadUri(uri);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await clickXpath('//*[@aria-label="Delete"]'); // Only visible close button is on the sprite
// Confirm that the stage has been switched to
await findByText('Stage selected: no motion blocks');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a sprite by uploading a png', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/gh-3582-png.png'));
await clickText('gh-3582-png', scope.spriteTile);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// This test fails because uploading an SVG as a sprite changes the scaling
// Enable when this is fixed issues/3608
test('Adding a sprite by uploading an svg (gh-3608)', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/100-100.svg'));
await clickText('100-100', scope.spriteTile); // Sprite is named for costume filename
// Check to make sure the size is right
await clickText('Costumes');
await clickText('100-100', scope.costumesTab); // The name of the costume
await clickText('100 x 100', scope.costumesTab); // The size of the costume
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a sprite by uploading a gif', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/paddleball.gif'));
await clickText('paddleball', scope.spriteTile); // Sprite is named for costume filename
await clickText('Costumes');
await findByText('paddleball', scope.costumesTab);
await findByText('paddleball2', scope.costumesTab);
await findByText('paddleball3', scope.costumesTab);
await findByText('paddleball4', scope.costumesTab);
await findByText('paddleball5', scope.costumesTab);
await findByText('paddleball6', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a letter sprite through the Letters filter in the library', async () => {
await loadUri(uri);
await driver.manage()
.window()
.setSize(1244, 768); // Letters filter not visible at 1024 width
await clickText('Costumes');
await clickXpath('//button[@aria-label="Choose a Sprite"]');
await clickText('Letters');
await clickText('Block-B', scope.modal); // Closes modal
await rightClickText('Block-B', scope.spriteTile); // Make sure it is there
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Use browser back button to close library', async () => {
await loadUri(uri);
await clickText('Costumes');
await clickXpath('//button[@aria-label="Choose a Sprite"]');
const abbyElement = await findByText('Abby'); // Should show editor for new costume
await elementIsVisible(abbyElement);
await driver.navigate().back();
// should throw error because library is no longer present
await expect(driver.wait(until.elementIsVisible(abbyElement)))
.rejects
.toBeInstanceOf(StaleElementReferenceError);
const costumesElement = await findByText('Costumes'); // Should show editor for new costume
await elementIsVisible(costumesElement);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding multiple sprites at the same time', async () => {
const files = [
path.resolve(__dirname, '../fixtures/gh-3582-png.png'),
path.resolve(__dirname, '../fixtures/100-100.svg')
];
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(files.join('\n'));
await findByText('gh-3582-png', scope.spriteTile);
await findByText('100-100', scope.spriteTile);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Load a sprite3 with a missing svg costume', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-svg.sprite3'));
const tile = await findByText('Blue Square Guy', scope.spriteTile);
const tileVisible = await tile.isDisplayed();
await expect(tileVisible).toBe(true);
});
test('Load a sprite3 with a currupt svg costume', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-svg.sprite3'));
const tile = await findByText('Blue Square Guy', scope.spriteTile);
const tileVisible = await tile.isDisplayed();
await expect(tileVisible).toBe(true);
});
test('Load a scratch3 corrupt svg as a sprite', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-from-scratch3.svg'));
const tile = await findByText('corrupt-from-scratch3', scope.spriteTile);
const tileVisible = await tile.isDisplayed();
await expect(tileVisible).toBe(true);
});
test('Load a sprite2 with a missing svg costume', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-svg.sprite2'));
const tile = await findByText('Blue Guy', scope.spriteTile);
const tileVisible = await tile.isDisplayed();
await expect(tileVisible).toBe(true);
});
test('Load a sprite2 with a currupt svg costume', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupted-svg.sprite2'));
const tile = await findByText('Blue Guy', scope.spriteTile);
const tileVisible = await tile.isDisplayed();
await expect(tileVisible).toBe(true);
});
test('Load a corrupt scratch2 svg as a sprite', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/scratch2-corrupted.svg'));
const tile = await findByText('scratch2-corrupted', scope.spriteTile);
const tileVisible = await tile.isDisplayed();
await expect(tileVisible).toBe(true);
});
test('Load a sprite3 with a missing bmp costume', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-bmp.sprite3'));
const tile = await findByText('green-bmp-guy', scope.spriteTile);
const tileVisible = await tile.isDisplayed();
await expect(tileVisible).toBe(true);
});
test('Load a sprite3 with a currupt bmp costume', async () => {
await loadUri(uri);
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
await driver.sleep(500); // Wait for thermometer menu to come up
const input = await findByXpath('//input[@type="file"]');
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-bmp.sprite3'));
const tile = await findByText('green-bmp-guy', scope.spriteTile);
const tileVisible = await tile.isDisplayed();
await expect(tileVisible).toBe(true);
});
// TODO: uploading a corrupt bmp as a sprite should throw an error and not add a gray question mark
});

View File

@@ -0,0 +1,46 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
rightClickText,
getDriver,
getLogs,
loadUri,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
describe('Loading scratch gui', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('Switching small/large stage after highlighting and deleting sprite', async () => {
await loadUri(uri);
// Highlight the sprite
await clickText('Sprite1', scope.spriteTile);
// Delete it
await rightClickText('Sprite1', scope.spriteTile);
await clickText('delete', scope.spriteTile);
// Go to small stage mode
await clickXpath('//button[@title="Switch to small stage"]');
// Confirm app still working
await clickXpath('//button[@title="Switch to large stage"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
});

View File

@@ -0,0 +1,38 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
findByXpath,
getDriver,
loadUri
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html?tutorial=all');
const uriPrefix = path.resolve(__dirname, '../../build/index.html?tutorial=');
let driver;
describe('Working with shortcut to Tutorials library', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('opens with the Tutorial Library showing', async () => {
await loadUri(uri);
// make sure there is a tutorial visible that doesn't have a shortcut
await clickText('Make It Spin');
await findByXpath('//div[contains(@class, "step-video")]');
});
test('can open hidden tutorials', async () => {
await loadUri(`${uriPrefix}whatsnew`);
// should open the tutorial video immediately
await findByXpath('//div[contains(@class, "step-video")]');
});
// @todo navigating cards, etc.
});