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,208 @@
// TODO: add tests of extension alerts
/* eslint-env jest */
import {AlertTypes, AlertLevels} from '../../../src/lib/alerts/index.jsx';
import alertsReducer, {
closeAlert,
closeAlertWithId,
filterInlineAlerts,
filterPopupAlerts,
showStandardAlert
} from '../../../src/reducers/alerts';
test('initialState', () => {
let defaultState;
/* alertsReducer(state, action) */
expect(alertsReducer(defaultState, {type: 'anything'})).toBeDefined();
expect(alertsReducer(defaultState, {type: 'anything'}).visible).toBe(true);
expect(alertsReducer(defaultState, {type: 'anything'}).alertsList).toEqual([]);
});
test('create one standard alert', () => {
let defaultState;
const action = showStandardAlert('creating');
const resultState = alertsReducer(defaultState, action);
expect(resultState.alertsList.length).toBe(1);
expect(resultState.alertsList[0].alertId).toBe('creating');
expect(resultState.alertsList[0].alertType).toBe(AlertTypes.STANDARD);
expect(resultState.alertsList[0].level).toBe(AlertLevels.SUCCESS);
});
test('add several standard alerts', () => {
const initialState = {
visible: true,
alertsList: [
{
alertId: 'saving',
alertType: AlertTypes.INLINE,
level: AlertLevels.SUCCESS,
content: null,
iconURL: '/no_image_here.jpg'
}
]
};
const action = showStandardAlert('creating');
let resultState = alertsReducer(initialState, action);
resultState = alertsReducer(resultState, action);
resultState = alertsReducer(resultState, action);
expect(resultState.alertsList.length).toBe(1);
expect(resultState.alertsList[0].alertType).toBe(AlertTypes.STANDARD);
expect(resultState.alertsList[0].iconURL).not.toBe('/no_image_here.jpg');
expect(resultState.alertsList[0].alertId).toBe('creating');
});
test('create one inline alert message', () => {
let defaultState;
const action = showStandardAlert('saving');
const resultState = alertsReducer(defaultState, action);
expect(resultState.alertsList.length).toBe(1);
expect(resultState.alertsList[0].alertId).toBe('saving');
expect(resultState.alertsList[0].alertType).toBe(AlertTypes.INLINE);
expect(resultState.alertsList[0].level).toBe(AlertLevels.INFO);
});
test('can close alerts by index', () => {
const initialState = {
visible: true,
alertsList: [
{
alertId: 'saving',
alertType: AlertTypes.INLINE,
level: AlertLevels.SUCCESS,
content: null,
iconURL: '/no_image_here.jpg'
}
]
};
const closeAction = closeAlert(0);
let resultState = alertsReducer(initialState, closeAction);
expect(resultState.alertsList.length).toBe(0);
const createAction = showStandardAlert('creating');
resultState = alertsReducer(resultState, createAction);
expect(resultState.alertsList.length).toBe(1);
resultState = alertsReducer(initialState, closeAction);
expect(resultState.alertsList.length).toBe(0);
resultState = alertsReducer(resultState, createAction);
});
test('can close a single alert by id', () => {
const initialState = {
visible: true,
alertsList: [
{alertId: 'saving'},
{alertId: 'creating'},
{alertId: 'saving'},
{alertId: 'saving'}
]
};
const closeAction = closeAlertWithId('saving');
let resultState = alertsReducer(initialState, closeAction);
expect(resultState.alertsList.map(a => a.alertId)).toEqual([
'creating', 'saving', 'saving'
]);
resultState = alertsReducer(resultState, closeAction);
expect(resultState.alertsList.map(a => a.alertId)).toEqual([
'creating', 'saving'
]);
resultState = alertsReducer(resultState, closeAction);
expect(resultState.alertsList.map(a => a.alertId)).toEqual([
'creating'
]);
resultState = alertsReducer(resultState, closeAction);
expect(resultState.alertsList.map(a => a.alertId)).toEqual([
'creating'
]);
});
test('related alerts can clear each other', () => {
const initialState = {
visible: true,
alertsList: [
{
alertId: 'saving',
alertType: AlertTypes.INLINE,
level: AlertLevels.SUCCESS,
content: null,
iconURL: '/no_image_here.jpg'
},
{
alertId: 'creating',
alertType: AlertTypes.STANDARD,
level: AlertLevels.SUCCESS,
content: null,
iconURL: '/no_image_here.jpg'
}
]
};
const action = showStandardAlert('saveSuccess');
const resultState = alertsReducer(initialState, action);
expect(resultState.alertsList.length).toBe(2);
expect(resultState.alertsList[0].alertId).toBe('creating');
expect(resultState.alertsList[1].alertId).toBe('saveSuccess');
});
test('several related alerts can be cleared at once', () => {
const initialState = {
visible: true,
alertsList: []
};
const createAction = showStandardAlert('creating');
let resultState = alertsReducer(initialState, createAction);
resultState = alertsReducer(resultState, createAction);
resultState = alertsReducer(resultState, createAction);
const createSuccessAction = showStandardAlert('createSuccess');
resultState = alertsReducer(resultState, createSuccessAction);
expect(resultState.alertsList.length).toBe(1);
expect(resultState.alertsList[0].alertId).toBe('createSuccess');
});
test('filterInlineAlerts only returns inline type alerts', () => {
const alerts = [
{
alertId: 'extension',
alertType: AlertTypes.EXTENSION
},
{
alertId: 'inline',
alertType: AlertTypes.INLINE
},
{
alertId: 'standard',
alertType: AlertTypes.STANDARD
},
{
alertId: 'non-existent type',
alertType: 'wirly-burly'
}
];
const filtered = filterInlineAlerts(alerts);
expect(filtered.length).toEqual(1);
expect(filtered[0].alertId).toEqual('inline');
});
test('filterPopupAlerts returns standard and extension type alerts', () => {
const alerts = [
{
alertId: 'extension',
alertType: AlertTypes.EXTENSION
},
{
alertId: 'inline',
alertType: AlertTypes.INLINE
},
{
alertId: 'standard',
alertType: AlertTypes.STANDARD
},
{
alertId: 'non-existent type',
alertType: 'wirly-burly'
}
];
const filtered = filterPopupAlerts(alerts);
expect(filtered.length).toEqual(2);
expect(filtered[0].alertId).toEqual('extension');
expect(filtered[1].alertId).toEqual('standard');
});

View File

@@ -0,0 +1,53 @@
/* eslint-env jest */
import modeReducer from '../../../src/reducers/mode';
const SET_FULL_SCREEN = 'scratch-gui/mode/SET_FULL_SCREEN';
const SET_PLAYER = 'scratch-gui/mode/SET_PLAYER';
test('initialState', () => {
let defaultState;
/* modeReducer(state, action) */
expect(modeReducer(defaultState, {type: 'anything'})).toBeDefined();
});
test('set full screen mode', () => {
const previousState = {
showBranding: false,
isFullScreen: false,
isPlayerOnly: false,
hasEverEnteredEditor: true
};
const action = {
type: SET_FULL_SCREEN,
isFullScreen: true
};
const newState = {
showBranding: false,
isFullScreen: true,
isPlayerOnly: false,
hasEverEnteredEditor: true
};
/* modeReducer(state, action) */
expect(modeReducer(previousState, action)).toEqual(newState);
});
test('set player mode', () => {
const previousState = {
showBranding: false,
isFullScreen: false,
isPlayerOnly: false,
hasEverEnteredEditor: true
};
const action = {
type: SET_PLAYER,
isPlayerOnly: true
};
const newState = {
showBranding: false,
isFullScreen: false,
isPlayerOnly: true,
hasEverEnteredEditor: true
};
/* modeReducer(state, action) */
expect(modeReducer(previousState, action)).toEqual(newState);
});

View File

@@ -0,0 +1,302 @@
/* eslint-env jest */
import monitorLayoutReducer, {
addMonitorRect, moveMonitorRect,
resizeMonitorRect, removeMonitorRect,
getInitialPosition, PADDING, SCREEN_WIDTH, SCREEN_HEIGHT
} from '../../../src/reducers/monitor-layout';
test('initialState', () => {
let defaultState;
expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */).monitors).toBeDefined();
expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */).savedMonitorPositions)
.toBeDefined();
});
test('addMonitorRect', () => {
let defaultState;
const monitorId = 1;
const monitorId2 = 2;
const upperStart = {x: 100, y: 100};
const lowerEnd = {x: 200, y: 200};
// Add a monitor rect
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd));
expect(reduxState.monitors[monitorId]).toBeDefined();
expect(reduxState.monitors[monitorId].upperStart).toEqual(upperStart);
expect(reduxState.monitors[monitorId].lowerEnd).toEqual(lowerEnd);
// Add monitor rect doesn't save position
expect(reduxState.savedMonitorPositions[monitorId]).toBeUndefined();
const reduxState2 = monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 0, 0));
// Add a second monitor rect
const reduxState3 = monitorLayoutReducer(reduxState2, addMonitorRect(monitorId2, upperStart, lowerEnd));
expect(reduxState3.monitors[monitorId]).toBeDefined();
expect(reduxState3.monitors[monitorId2]).toBeDefined();
expect(reduxState3.monitors[monitorId2].upperStart).toEqual(upperStart);
expect(reduxState3.monitors[monitorId2].lowerEnd).toEqual(lowerEnd);
// Saved positions aren't changed by adding monitor
expect(reduxState3.savedMonitorPositions).toEqual(reduxState2.savedMonitorPositions);
});
test('addMonitorRectWithSavedPosition', () => {
let defaultState;
const monitorId = 1;
const upperStart = {x: 100, y: 100};
const lowerEnd = {x: 200, y: 200};
// Add a monitor rect
const reduxState = monitorLayoutReducer(defaultState,
addMonitorRect(monitorId, upperStart, lowerEnd, true /* savePosition */));
expect(reduxState.monitors[monitorId]).toBeDefined();
expect(reduxState.monitors[monitorId].upperStart).toEqual(upperStart);
expect(reduxState.monitors[monitorId].lowerEnd).toEqual(lowerEnd);
// Save position
expect(reduxState.savedMonitorPositions[monitorId].x).toEqual(upperStart.x);
expect(reduxState.savedMonitorPositions[monitorId].y).toEqual(upperStart.y);
});
test('invalidRect', () => {
let defaultState;
const reduxState = monitorLayoutReducer(defaultState /* state */, {type: 'initialize'} /* action */);
// Problem: x end is before x start
expect(
monitorLayoutReducer(reduxState,
addMonitorRect(1, {x: 100, y: 100}, {x: 10, y: 200})))
.toEqual(reduxState);
// Problem: y end is before y start
expect(
monitorLayoutReducer(reduxState,
addMonitorRect(1, {x: 100, y: 100}, {x: 200, y: 10})))
.toEqual(reduxState);
});
test('invalidAddMonitorRect', () => {
let defaultState;
const monitorId = 1;
const upperStart = {x: 100, y: 100};
const lowerEnd = {x: 200, y: 200};
// Add a monitor rect
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd));
// Try to add the same one
expect(monitorLayoutReducer(reduxState, addMonitorRect(monitorId, upperStart, lowerEnd)))
.toEqual(reduxState);
});
test('moveMonitorRect', () => {
let defaultState;
const monitorId = 1;
const monitorId2 = 2;
const width = 102;
const height = 101;
const upperStart = {x: 100, y: 100};
const lowerEnd = {x: upperStart.x + width, y: upperStart.y + height};
const movedToPosition = {x: 0, y: 0};
const movedToPosition2 = {x: 543, y: 2};
// Add a monitor rect and move it. Expect it to be in monitors state and saved positions.
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd));
const reduxState2 = monitorLayoutReducer(reduxState,
moveMonitorRect(monitorId, movedToPosition.x, movedToPosition.y));
expect(reduxState2.monitors[monitorId]).toBeDefined();
expect(reduxState2.monitors[monitorId].upperStart).toEqual(movedToPosition);
expect(reduxState2.monitors[monitorId].lowerEnd.x).toEqual(movedToPosition.x + width);
expect(reduxState2.monitors[monitorId].lowerEnd.y).toEqual(movedToPosition.y + height);
expect(reduxState2.savedMonitorPositions[monitorId]).toBeDefined();
expect(reduxState2.savedMonitorPositions[monitorId].x).toEqual(movedToPosition.x);
expect(reduxState2.savedMonitorPositions[monitorId].y).toEqual(movedToPosition.y);
// Add a second monitor rect and move it. Expect there to now be 2 saved positions.
const reduxState3 = monitorLayoutReducer(reduxState2, addMonitorRect(monitorId2, upperStart, lowerEnd));
const reduxState4 = monitorLayoutReducer(reduxState3,
moveMonitorRect(monitorId2, movedToPosition2.x, movedToPosition2.y));
expect(reduxState4.savedMonitorPositions[monitorId]).toEqual(reduxState2.savedMonitorPositions[monitorId]);
expect(reduxState4.savedMonitorPositions[monitorId2].x).toEqual(movedToPosition2.x);
expect(reduxState4.savedMonitorPositions[monitorId2].y).toEqual(movedToPosition2.y);
});
test('invalidMoveMonitorRect', () => {
let defaultState;
let reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */);
const monitorId = 1;
// Try to move a monitor rect that doesn't exist
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newX */, 1 /* newY */)))
.toEqual(reduxState);
// Add the monitor to move
reduxState = monitorLayoutReducer(reduxState, addMonitorRect(monitorId, {x: 100, y: 100}, {x: 200, y: 200}));
// Invalid newX
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 'Oregon' /* newX */, 1 /* newY */)))
.toEqual(reduxState);
// Invalid newY
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newX */)))
.toEqual(reduxState);
});
test('resizeMonitorRect', () => {
let defaultState;
const monitorId = 1;
const upperStart = {x: 100, y: 100};
const newWidth = 10;
const newHeight = 20;
// Add a monitor rect and resize it
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, {x: 200, y: 200}));
const reduxState2 = monitorLayoutReducer(reduxState,
resizeMonitorRect(monitorId, newWidth, newHeight));
expect(reduxState2.monitors[monitorId]).toBeDefined();
expect(reduxState2.monitors[monitorId].upperStart).toEqual(upperStart);
expect(reduxState2.monitors[monitorId].lowerEnd.x).toEqual(upperStart.x + newWidth);
expect(reduxState2.monitors[monitorId].lowerEnd.y).toEqual(upperStart.y + newHeight);
// Saved positions aren't changed by resizing monitor
expect(reduxState2.savedMonitorPositions).toEqual(reduxState.savedMonitorPositions);
});
test('invalidResizeMonitorRect', () => {
let defaultState;
let reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */);
const monitorId = 1;
// Try to resize a monitor rect that doesn't exist
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 1 /* newWidth */, 1 /* newHeight */)))
.toEqual(reduxState);
// Add the monitor to resize
reduxState = monitorLayoutReducer(reduxState, addMonitorRect(monitorId, {x: 100, y: 100}, {x: 200, y: 200}));
// Invalid newWidth
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 'Oregon' /* newWidth */, 1 /* newHeight */)))
.toEqual(reduxState);
// Invalid newHeight
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newWidth */)))
.toEqual(reduxState);
// newWidth < 0
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, -1 /* newWidth */, 1 /* newHeight */)))
.toEqual(reduxState);
// newHeight < 0
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 1 /* newWidth */, -1 /* newHeight */)))
.toEqual(reduxState);
});
test('removeMonitorRect', () => {
let defaultState;
const monitorId = 1;
// Add a monitor rect, move it, and remove it
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId,
{x: 100, y: 100},
{x: 200, y: 200}
));
const reduxState2 = monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 0, 0));
const reduxState3 = monitorLayoutReducer(reduxState2, removeMonitorRect(monitorId));
expect(reduxState3.monitors[monitorId]).toBeUndefined();
// Check that saved positions aren't changed by removing monitor
expect(reduxState3.savedMonitorPositions).toEqual(reduxState2.savedMonitorPositions);
});
test('invalidRemoveMonitorRect', () => {
let defaultState;
const reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */);
// Try to remove a monitor rect that doesn't exist
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(1)))
.toEqual(reduxState);
});
test('getInitialPosition_lineUpTopLeft', () => {
let defaultState;
const width = 100;
const height = 200;
// Add monitors to right and bottom, but there is a space in the top left
let reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1,
{x: width + PADDING, y: 0},
{x: 100, y: height}
));
reduxState = monitorLayoutReducer(defaultState, addMonitorRect(2,
{x: 0, y: height + PADDING},
{x: width, y: 100}
));
// Check that the added monitor appears in the space
const rect = getInitialPosition(reduxState, 3, width, height);
expect(rect.upperStart).toBeDefined();
expect(rect.lowerEnd).toBeDefined();
expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width);
expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height);
expect(rect.upperStart.x).toEqual(PADDING);
expect(rect.upperStart.y).toEqual(PADDING);
});
test('getInitialPosition_savedPosition', () => {
const monitorId = 1;
const savedX = 100;
const savedY = 200;
const width = 7;
const height = 8;
const reduxState = {
monitors: {},
savedMonitorPositions: {[monitorId]: {x: savedX, y: savedY}}
};
// Check that initial position uses saved state
const rect = getInitialPosition(reduxState, monitorId, width, height);
expect(rect.upperStart).toBeDefined();
expect(rect.lowerEnd).toBeDefined();
expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width);
expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height);
expect(rect.upperStart.x).toEqual(savedX);
expect(rect.upperStart.y).toEqual(savedY);
});
test('getInitialPosition_lineUpLeft', () => {
let defaultState;
const monitor1EndY = 60;
// Add a monitor that takes up the upper left corner
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1, {x: 0, y: 0}, {x: 100, y: monitor1EndY}));
// Check that added monitor is under it and lines up left
const rect = getInitialPosition(reduxState, 2, 20 /* width */, 20 /* height */);
expect(rect.upperStart.y >= monitor1EndY + PADDING).toBeTruthy();
});
test('getInitialPosition_lineUpTop', () => {
let defaultState;
const monitor1EndX = 100;
// Add a monitor that takes up the whole left side
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1,
{x: 0, y: 0},
{x: monitor1EndX, y: SCREEN_HEIGHT}
));
// Check that added monitor is to the right of it and lines up top
const rect = getInitialPosition(reduxState, 2, 20 /* width */, 20 /* height */);
expect(rect.upperStart.y).toEqual(PADDING);
expect(rect.upperStart.x >= monitor1EndX + PADDING).toBeTruthy();
});
test('getInitialPosition_noRoom', () => {
let defaultState;
const width = 7;
const height = 8;
// Add a monitor that takes up the whole screen
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1,
{x: 0, y: 0},
{x: SCREEN_WIDTH, y: SCREEN_HEIGHT}
));
// Check that added monitor exists somewhere (we don't care where)
const rect = getInitialPosition(reduxState, 2, width, height);
expect(rect.upperStart).toBeDefined();
expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width);
expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height);
});

View File

@@ -0,0 +1,508 @@
/* eslint-env jest */
import projectStateReducer, {
LoadingState,
autoUpdateProject,
doneCreatingProject,
doneUpdatingProject,
manualUpdateProject,
onFetchedProjectData,
onLoadedProject,
projectError,
remixProject,
requestNewProject,
requestProjectUpload,
saveProjectAsCopy,
setProjectId
} from '../../../src/reducers/project-state';
test('initialState', () => {
let defaultState;
/* projectStateReducer(state, action) */
expect(projectStateReducer(defaultState, {type: 'anything'})).toBeDefined();
expect(projectStateReducer(defaultState, {type: 'anything'}).error).toBe(null);
expect(projectStateReducer(defaultState, {type: 'anything'}).projectData).toBe(null);
expect(projectStateReducer(defaultState, {type: 'anything'}).projectId).toBe(null);
expect(projectStateReducer(defaultState, {type: 'anything'}).loadingState).toBe(LoadingState.NOT_LOADED);
});
test('doneCreatingProject for new project with projectId type string shows project with that id', () => {
const initialState = {
projectId: null,
loadingState: LoadingState.CREATING_NEW
};
const action = doneCreatingProject('100', initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
test('doneCreatingProject for new project with projectId type number shows project with id of type number', () => {
const initialState = {
projectId: null,
loadingState: LoadingState.CREATING_NEW
};
const action = doneCreatingProject(100, initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe(100);
});
test('doneCreatingProject for remix shows project with that id', () => {
const initialState = {
projectId: null,
loadingState: LoadingState.REMIXING
};
const action = doneCreatingProject('100', initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
test('doneCreatingProject for save as copy shows project with that id', () => {
const initialState = {
projectId: null,
loadingState: LoadingState.CREATING_COPY
};
const action = doneCreatingProject('100', initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
test('onFetchedProjectData with id loads project data into vm', () => {
const initialState = {
projectData: null,
loadingState: LoadingState.FETCHING_WITH_ID
};
const action = onFetchedProjectData('1010101', initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_WITH_ID);
expect(resultState.projectData).toBe('1010101');
});
test('onFetchedProjectData new loads project data into vm', () => {
const initialState = {
projectData: null,
loadingState: LoadingState.FETCHING_NEW_DEFAULT
};
const action = onFetchedProjectData('1010101', initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_NEW_DEFAULT);
expect(resultState.projectData).toBe('1010101');
});
// onLoadedProject: LOADING_VM_WITH_ID
test('onLoadedProject(LOADING_VM_WITH_ID, true, true) results in state SHOWING_WITH_ID', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.LOADING_VM_WITH_ID
};
const action = onLoadedProject(initialState.loadingState, true, true);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
test('onLoadedProject(LOADING_VM_WITH_ID, false, true) results in state SHOWING_WITH_ID', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.LOADING_VM_WITH_ID
};
const action = onLoadedProject(initialState.loadingState, false, true);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
// case where we started out viewing a project with a projectId, then
// started to load another project; but loading fails. We go back to
// showing the original project.
test('onLoadedProject(LOADING_VM_WITH_ID, false, false), with project id, ' +
'results in state SHOWING_WITH_ID', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.LOADING_VM_WITH_ID
};
const action = onLoadedProject(initialState.loadingState, false, false);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
// case where we started out viewing a project with default projectId, then
// started to load one with an id, such as in standalone mode when user adds
// '#PROJECT_ID_NUMBER' to the URI; but loading fails. We go back to
// showing the original project.
test('onLoadedProject(LOADING_VM_WITH_ID, false, false), with no project id, ' +
'results in state SHOWING_WITHOUT_ID', () => {
const initialState = {
projectId: '0',
loadingState: LoadingState.LOADING_VM_WITH_ID
};
const action = onLoadedProject(initialState.loadingState, false, false);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
expect(resultState.projectId).toBe('0');
});
// onLoadedProject: LOADING_VM_FILE_UPLOAD
test('onLoadedProject(LOADING_VM_FILE_UPLOAD, true, true) prepares to save', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
};
const action = onLoadedProject(initialState.loadingState, true, true);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.AUTO_UPDATING);
expect(resultState.projectId).toBe('100');
});
test('onLoadedProject(LOADING_VM_FILE_UPLOAD, false, true) results in state SHOWING_WITHOUT_ID', () => {
const initialState = {
projectId: '0',
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
};
const action = onLoadedProject(initialState.loadingState, false, true);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
expect(resultState.projectId).toBe('0');
});
test('onLoadedProject(LOADING_VM_FILE_UPLOAD, false, false), when we know project id, ' +
'results in state SHOWING_WITH_ID', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
};
const action = onLoadedProject(initialState.loadingState, false, false);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
test('onLoadedProject(LOADING_VM_FILE_UPLOAD, false, false), when we ' +
'don\'t know project id, results in state SHOWING_WITHOUT_ID', () => {
const initialState = {
projectId: '0',
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
};
const action = onLoadedProject(initialState.loadingState, false, false);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
expect(resultState.projectId).toBe('0');
});
// onLoadedProject: LOADING_VM_NEW_DEFAULT
test('onLoadedProject(LOADING_VM_NEW_DEFAULT, true, true) results in state SHOWING_WITHOUT_ID', () => {
const initialState = {
projectId: '0',
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
};
const action = onLoadedProject(initialState.loadingState, true, true);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
expect(resultState.projectId).toBe('0');
});
test('onLoadedProject(LOADING_VM_NEW_DEFAULT, false, true) results in state SHOWING_WITHOUT_ID', () => {
const initialState = {
projectId: '0',
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
};
const action = onLoadedProject(initialState.loadingState, false, true);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
expect(resultState.projectId).toBe('0');
});
test('onLoadedProject(LOADING_VM_NEW_DEFAULT, false, false) results in ERROR state', () => {
const initialState = {
projectId: '0',
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
};
const action = onLoadedProject(initialState.loadingState, false, false);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.ERROR);
expect(resultState.projectId).toBe('0');
});
// doneUpdatingProject
test('doneUpdatingProject with id results in state SHOWING_WITH_ID', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.MANUAL_UPDATING
};
const action = doneUpdatingProject(initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
test('doneUpdatingProject with id, before copy occurs, results in state CREATING_COPY', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.UPDATING_BEFORE_COPY
};
const action = doneUpdatingProject(initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.CREATING_COPY);
expect(resultState.projectId).toBe('100');
});
test('doneUpdatingProject with id, before new, results in state FETCHING_NEW_DEFAULT', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.UPDATING_BEFORE_NEW
};
const action = doneUpdatingProject(initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.FETCHING_NEW_DEFAULT);
expect(resultState.projectId).toBe('0'); // resets id
});
test('calling setProjectId, using with same id as already showing, ' +
'results in state SHOWING_WITH_ID', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.SHOWING_WITH_ID
};
const action = setProjectId('100');
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
test('calling setProjectId, using different id from project already showing, ' +
'results in state FETCHING_WITH_ID', () => {
const initialState = {
projectId: 99,
loadingState: LoadingState.SHOWING_WITH_ID
};
const action = setProjectId('100');
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.FETCHING_WITH_ID);
expect(resultState.projectId).toBe('100');
});
test('setProjectId, with same id as before, but not same type, ' +
'results in FETCHING_WITH_ID because the two projectIds are not strictly equal', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.SHOWING_WITH_ID
};
const action = setProjectId(100);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.FETCHING_WITH_ID);
expect(resultState.projectId).toBe(100);
});
test('requestNewProject, when can\'t create/save, results in FETCHING_NEW_DEFAULT', () => {
const initialState = {
projectId: '0',
loadingState: LoadingState.SHOWING_WITHOUT_ID
};
const action = requestNewProject(false);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.FETCHING_NEW_DEFAULT);
expect(resultState.projectId).toBe('0');
});
test('requestNewProject, when can create/save, results in UPDATING_BEFORE_NEW ' +
'(in order to save before fetching default project)', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.SHOWING_WITH_ID
};
const action = requestNewProject(true);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.UPDATING_BEFORE_NEW);
expect(resultState.projectId).toBe('100');
});
test('requestProjectUpload when project not loaded results in state LOADING_VM_FILE_UPLOAD', () => {
const initialState = {
projectId: null,
loadingState: LoadingState.NOT_LOADED
};
const action = requestProjectUpload(initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_FILE_UPLOAD);
expect(resultState.projectId).toBe(null);
});
test('requestProjectUpload when showing project with id results in state LOADING_VM_FILE_UPLOAD', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.SHOWING_WITH_ID
};
const action = requestProjectUpload(initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_FILE_UPLOAD);
expect(resultState.projectId).toBe('100');
});
test('requestProjectUpload when showing project without id results in state LOADING_VM_FILE_UPLOAD', () => {
const initialState = {
projectId: null,
loadingState: LoadingState.SHOWING_WITHOUT_ID
};
const action = requestProjectUpload(initialState.loadingState);
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_FILE_UPLOAD);
expect(resultState.projectId).toBe(null);
});
test('manualUpdateProject should prepare to update', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.SHOWING_WITH_ID
};
const action = manualUpdateProject();
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.MANUAL_UPDATING);
expect(resultState.projectId).toBe('100');
});
test('autoUpdateProject should prepare to update', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.SHOWING_WITH_ID
};
const action = autoUpdateProject();
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.AUTO_UPDATING);
expect(resultState.projectId).toBe('100');
});
test('saveProjectAsCopy should save, before preparing to save as a copy', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.SHOWING_WITH_ID
};
const action = saveProjectAsCopy();
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.UPDATING_BEFORE_COPY);
expect(resultState.projectId).toBe('100');
});
test('remixProject should prepare to remix', () => {
const initialState = {
projectId: '100',
loadingState: LoadingState.SHOWING_WITH_ID
};
const action = remixProject();
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.REMIXING);
expect(resultState.projectId).toBe('100');
});
test('projectError from various states should show error', () => {
const startStates = [
LoadingState.AUTO_UPDATING,
LoadingState.CREATING_NEW,
LoadingState.FETCHING_NEW_DEFAULT,
LoadingState.FETCHING_WITH_ID,
LoadingState.LOADING_VM_NEW_DEFAULT,
LoadingState.LOADING_VM_WITH_ID,
LoadingState.MANUAL_UPDATING,
LoadingState.REMIXING,
LoadingState.CREATING_COPY,
LoadingState.UPDATING_BEFORE_NEW
];
for (const startState of startStates) {
const initialState = {
error: null,
projectId: '100',
loadingState: startState
};
const action = projectError('Error string');
const resultState = projectStateReducer(initialState, action);
expect(resultState.error).toEqual('Error string');
expect(resultState.projectId).toBe('100');
}
});
test('fatal projectError should show error state', () => {
const startStates = [
LoadingState.FETCHING_NEW_DEFAULT,
LoadingState.FETCHING_WITH_ID,
LoadingState.LOADING_VM_NEW_DEFAULT,
LoadingState.LOADING_VM_WITH_ID
];
for (const startState of startStates) {
const initialState = {
error: null,
projectId: '100',
loadingState: startState
};
const action = projectError('Error string');
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.ERROR);
expect(resultState.projectId).toBe('100');
}
});
test('non-fatal projectError should show normal state', () => {
const startStates = [
LoadingState.AUTO_UPDATING,
LoadingState.CREATING_COPY,
LoadingState.MANUAL_UPDATING,
LoadingState.REMIXING,
LoadingState.UPDATING_BEFORE_NEW
];
for (const startState of startStates) {
const initialState = {
error: null,
projectId: '100',
loadingState: startState
};
const action = projectError('Error string');
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('100');
}
});
test('projectError when creating new while viewing project with id should ' +
'go back to state SHOWING_WITH_ID', () => {
const initialState = {
error: null,
loadingState: LoadingState.CREATING_NEW,
projectId: '12345'
};
const action = projectError('Error string');
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
expect(resultState.projectId).toBe('12345');
});
test('projectError when creating new while logged out, looking at default project ' +
'should go back to state SHOWING_WITHOUT_ID', () => {
const initialState = {
error: null,
loadingState: LoadingState.CREATING_NEW,
projectId: '0'
};
const action = projectError('Error string');
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
expect(resultState.projectId).toBe('0');
});
test('projectError encountered while in state FETCHING_WITH_ID results in ' +
'ERROR state', () => {
const initialState = {
error: null,
projectId: null,
loadingState: LoadingState.FETCHING_WITH_ID
};
const action = projectError('Error string');
const resultState = projectStateReducer(initialState, action);
expect(resultState.loadingState).toBe(LoadingState.ERROR);
expect(resultState.projectId).toBe(null);
expect(resultState.error).toEqual('Error string');
});

View File

@@ -0,0 +1,25 @@
/* eslint-env jest */
import workspaceMetricsReducer, {updateMetrics} from '../../../src/reducers/workspace-metrics';
test('initialState', () => {
let defaultState;
/* workspaceMetricsReducer(state, action) */
expect(workspaceMetricsReducer(defaultState, {type: 'anything'})).toBeDefined();
expect(workspaceMetricsReducer(defaultState, {type: 'anything'})).toEqual({targets: {}});
});
test('updateMetrics action creator', () => {
let defaultState;
const action = updateMetrics({
targetID: 'abcde',
scrollX: 225,
scrollY: 315,
scale: 1.25
});
const resultState = workspaceMetricsReducer(defaultState, action);
expect(Object.keys(resultState.targets).length).toBe(1);
expect(resultState.targets.abcde).toBeDefined();
expect(resultState.targets.abcde.scrollX).toBe(225);
expect(resultState.targets.abcde.scrollY).toBe(315);
expect(resultState.targets.abcde.scale).toBe(1.25);
});