diff --git a/cypress.json b/cypress.json
new file mode 100644
index 0000000..25f81c5
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1,3 @@
+{
+ "chromeWebSecurity": false
+}
diff --git a/cypress/fixtures/libraries.json b/cypress/fixtures/libraries.json
new file mode 100644
index 0000000..c7b05dc
--- /dev/null
+++ b/cypress/fixtures/libraries.json
@@ -0,0 +1,38 @@
+{
+ "jsLibs": [
+ {
+ "urlPref": "https://code.jquery.com/jquery",
+ "label": "jQuery"
+ },
+ {
+ "urlPref": "https://ajax.googleapis.com/ajax/libs/angularjs/",
+ "label": "Angular"
+ },
+ {
+ "urlPref": "https://cdnjs.cloudflare.com/ajax/libs/react/",
+ "label": "React"
+ },
+ {
+ "urlPref": "https://cdnjs.cloudflare.com/ajax/libs/react-dom/",
+ "label": "React DOM"
+ },
+ {
+ "urlPref": "https://unpkg.com/vue/dist/vue.min.js",
+ "label": "Vue.js"
+ }
+ ],
+ "cssLibs": [
+ {
+ "urlPref": "https://cdnjs.cloudflare.com/ajax/libs/bulma/",
+ "label": "Bulma"
+ },
+ {
+ "urlPref": "https://cdnjs.cloudflare.com/ajax/libs/hint.css/",
+ "label": "Hint.css"
+ },
+ {
+ "urlPref": "https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css",
+ "label": "Tailwind.css"
+ }
+ ]
+}
diff --git a/cypress/integration/console.test.js b/cypress/integration/console.test.js
new file mode 100644
index 0000000..239d5d1
--- /dev/null
+++ b/cypress/integration/console.test.js
@@ -0,0 +1,64 @@
+///
+
+const consoleAssert = (prompt, expected) => {
+ cy.get('#consolePromptEl').type(`${prompt}{enter}`);
+
+ // append the prompt at the beginning of the expected items
+ expected.unshift(`"> ${prompt}"`);
+
+ cy.get('ul[data-testid=consoleItems] li').should(
+ 'have.length',
+ expected.length
+ );
+
+ expected.forEach((expectedValue, index) => {
+ cy.get('ul[data-testid=consoleItems] li').eq(index).contains(expectedValue);
+ });
+};
+
+describe('Console checks', () => {
+ beforeEach(() => {
+ cy.init();
+
+ cy.get('a[data-testid=toggleConsole]').click();
+ });
+
+ it('Simple arithmetic addition', () => {
+ consoleAssert('4+5', ['9']);
+ });
+
+ it('Simple arithmetic subtraction', () => {
+ consoleAssert('4-5', ['-1']);
+ });
+
+ it('Simple arithmetic multiplication', () => {
+ consoleAssert('4*5', ['20']);
+ });
+
+ it('Simple arithmetic division', () => {
+ consoleAssert('4/5', ['0.8']);
+ });
+
+ it('Division by Zero', () => {
+ consoleAssert('4/0', ['Infinity']);
+ });
+
+ it('Console log a message', () => {
+ consoleAssert("console.log('hello')", ['hello', 'undefined']);
+ });
+
+ it('Equality check', () => {
+ consoleAssert("100 == '100'", ['true']);
+ });
+
+ it('Strict-Equality check', () => {
+ consoleAssert("100 === '100'", ['false']);
+ });
+
+ it.only('Minimizing console', () => {
+ cy.get('#consoleEl').should('not.have.class', 'is-minimized');
+ cy.wait(200); // wait for animation to complete
+ cy.get('a[data-testid=toggleConsole]').click();
+ cy.get('#consoleEl').should('have.class', 'is-minimized');
+ });
+});
diff --git a/cypress/integration/interface.test.js b/cypress/integration/interface.test.js
new file mode 100644
index 0000000..8030da8
--- /dev/null
+++ b/cypress/integration/interface.test.js
@@ -0,0 +1,175 @@
+///
+
+describe('Testing interfaces', () => {
+ beforeEach(() => {
+ cy.init();
+ });
+
+ it('New button should create a new item if no unsaved changes present', () => {
+ cy.get('[data-testid=newButton]').click();
+
+ // since we haven't made any changed it should show the modal
+ cy.get('.modal__content').should('be.visible');
+ });
+
+ it('New button should ask for confirmation if unsaved changes present, when confirmed modal should pop-up', () => {
+ cy.get('#htmlCodeEl').type('Hello');
+ cy.get('[data-testid=newButton]').click();
+
+ cy.on('window:confirm', text => {
+ expect(text).to.contains('You have unsaved changes.');
+ });
+ cy.get('.modal__content').should('be.visible');
+ });
+
+ it('New button should ask for confirmation if unsaved changes present, when declined modal should not pop-up', () => {
+ cy.get('#htmlCodeEl').type('Hello');
+ cy.get('[data-testid=newButton]').click();
+
+ cy.on('window:confirm', text => {
+ expect(text).to.contains('You have unsaved changes.');
+ return false;
+ });
+ cy.get('.modal__content').should('not.exist');
+ });
+
+ it('Save button click should save the current work with a notification.', () => {
+ const sampleText = 'Hello';
+ cy.get('#htmlCodeEl').type(sampleText);
+
+ cy.on('window:confirm', text => {
+ expect(text).to.contains('Do you still want to continue saving locally?');
+ });
+
+ cy.get('#saveBtn').click();
+ cy.get('#js-alerts-container').should('be.visible');
+ cy.get('#js-alerts-container').contains('Auto-save enabled');
+
+ cy.then(() => {
+ const ls = JSON.parse(localStorage.getItem('code'));
+ expect(ls).to.be.not.null;
+ expect(ls['title']).to.contain('Untitled');
+ expect(ls['html']).to.eq(sampleText);
+ });
+ });
+
+ it('Cmd + S (Save) should save the current work with a notification.', () => {
+ const sampleText = 'Hello';
+
+ cy.on('window:confirm', text => {
+ expect(text).to.contains('Do you still want to continue saving locally?');
+ });
+
+ cy.get('#htmlCodeEl').type(sampleText + '{ctrl+s}');
+ cy.get('#js-alerts-container').should('be.visible');
+ cy.get('#js-alerts-container').contains('Auto-save enabled');
+
+ cy.then(() => {
+ const ls = JSON.parse(localStorage.getItem('code'));
+ expect(ls).to.be.not.null;
+ expect(ls['title']).to.contain('Untitled');
+ expect(ls['html']).to.eq(sampleText);
+ });
+ });
+
+ it('Changing creation title should auto save in localstorage', () => {
+ const sampleText = 'Hello';
+ cy.get('#htmlCodeEl').type(sampleText);
+
+ cy.on('window:confirm', text => {
+ expect(text).to.contains('Do you still want to continue saving locally?');
+ });
+
+ cy.get('#saveBtn').click();
+
+ cy.then(() => {
+ const ls = JSON.parse(localStorage.getItem('code'));
+ expect(ls).to.be.not.null;
+ expect(ls['title']).to.contain('Untitled');
+ expect(ls['html']).to.eq(sampleText);
+ });
+
+ cy.get('#titleInput').clear().type('test');
+
+ cy.get('#saveBtn').click();
+ cy.get('#js-alerts-container').should('be.visible');
+ cy.get('#js-alerts-container').contains('Item saved');
+
+ cy.wait(1000); // for the localstorage to reflect the changes
+
+ cy.then(() => {
+ const ls = JSON.parse(localStorage.getItem('code'));
+ expect(ls).to.be.not.null;
+ expect(ls['title']).to.eq('test');
+ expect(ls['html']).to.eq(sampleText);
+ });
+ });
+
+ it('Clicking "OPEN" should open the saved items pane', () => {
+ cy.on('window:confirm', text => {
+ expect(text).to.contains('Do you still want to continue saving locally?');
+ return true;
+ });
+
+ const addCreation = message => {
+ // start with blank project
+ cy.get('[data-testid=newButton').click();
+ cy.get('[data-testid=startBlankButton]').click();
+
+ // type a message in the HTML section
+ cy.get('#htmlCodeEl').type('{ctrl+a}{backspace}' + message);
+ // type the title
+ cy.get('#titleInput').clear().type(message).blur();
+
+ // save it
+ cy.get('#saveBtn').click();
+ cy.wait(1000);
+ cy.then(() => {
+ const ls = JSON.parse(localStorage.getItem('code'));
+ console.log(ls);
+ expect(ls).to.be.not.null;
+ expect(ls['title']).to.eq(message);
+ expect(ls['html']).to.eq(message);
+ });
+ };
+
+ // save some projects
+ const messages = ['test', 'test2', 'abc'];
+ messages.forEach(m => addCreation(m));
+
+ // check for the saved projects
+ cy.get('#openItemsBtn').click();
+ cy.get('#js-saved-items-wrap').should('be.visible');
+ messages.forEach((m, index) => {
+ cy.get('#js-saved-items-wrap')
+ .children()
+ .eq(messages.length - index - 1)
+ .contains(m);
+ });
+ });
+
+ it('Selecting a library from dropdown should change URL in textarea', () => {
+ cy.get('[data-testid=addLibraryButton]').click();
+
+ cy.get('#externalJsTextarea').should('exist');
+ cy.get('#externalCssTextarea').should('exist');
+
+ const checkLibrary = (label, url, type) => {
+ cy.get('#js-add-library-select').select(label);
+
+ cy.get(`#external${type}Textarea`).should('contain.value', '\n' + url); // \n because every url should be in a new line
+ };
+
+ cy.fixture('libraries').then(data => {
+ data['jsLibs'].forEach(lib =>
+ checkLibrary(lib['label'], lib['urlPref'], 'Js')
+ );
+ });
+
+ cy.fixture('libraries').then(data => {
+ data['cssLibs'].forEach(lib =>
+ checkLibrary(lib['label'], lib['urlPref'], 'Css')
+ );
+ });
+ });
+});
diff --git a/cypress/integration/layouts.test.js b/cypress/integration/layouts.test.js
new file mode 100644
index 0000000..26e295d
--- /dev/null
+++ b/cypress/integration/layouts.test.js
@@ -0,0 +1,94 @@
+///
+
+describe('Pressing the layout buttons should change the layouts accordingly', () => {
+ before(() => {
+ cy.init();
+ cy.viewport(1270, 720);
+ });
+
+ const getLayoutBtnId = index => `#layoutBtn${index}`;
+
+ it('Default Layout', () => {
+ cy.get('body').should('have.class', 'layout-1');
+
+ cy.get('#js-code-side').should('be.visible');
+ cy.get('#js-demo-side').should('be.visible');
+
+ cy.get('#js-code-side').should(
+ 'have.attr',
+ 'style',
+ 'width: calc(50% - 3px);'
+ );
+ cy.get('#js-code-side').should('have.attr', 'direction', 'vertical');
+
+ cy.get('#js-demo-side').should(
+ 'have.attr',
+ 'style',
+ 'width: calc(50% - 3px);'
+ );
+ });
+
+ it('Layout 2', () => {
+ cy.get(getLayoutBtnId(2)).click();
+
+ cy.get('body').should('have.class', 'layout-2');
+
+ cy.get('#js-code-side').should('be.visible');
+ cy.get('#js-demo-side').should('be.visible');
+
+ cy.get('#js-code-side').should(
+ 'have.attr',
+ 'style',
+ 'height: calc(50% - 3px);'
+ );
+ cy.get('#js-code-side').should('have.attr', 'direction', 'horizontal');
+
+ cy.get('#js-demo-side').should(
+ 'have.attr',
+ 'style',
+ 'height: calc(50% - 3px);'
+ );
+ });
+
+ it('Layout 3', () => {
+ cy.get(getLayoutBtnId(3)).click();
+
+ cy.get('body').should('have.class', 'layout-3');
+
+ cy.get('#js-code-side').should('be.visible');
+ cy.get('#js-demo-side').should('be.visible');
+
+ cy.get('#js-code-side').should(
+ 'have.attr',
+ 'style',
+ 'width: calc(50% - 3px);'
+ );
+ cy.get('#js-code-side').should('have.attr', 'direction', 'vertical');
+
+ cy.get('#js-demo-side').should(
+ 'have.attr',
+ 'style',
+ 'width: calc(50% - 3px);'
+ );
+ });
+
+ it('Layout 4', () => {
+ cy.get(getLayoutBtnId(4)).click();
+
+ cy.get('body').should('have.class', 'layout-4');
+
+ cy.get('#js-code-side').should('not.be.visible');
+ cy.get('#js-demo-side').should('be.visible');
+ });
+
+ it('Layout 5', () => {
+ cy.get(getLayoutBtnId(5)).click();
+
+ cy.get('body').should('have.class', 'layout-5');
+
+ cy.get('#js-code-side').should('be.visible');
+ cy.get('#js-demo-side').should('be.visible');
+
+ cy.get('#js-code-side').should('have.attr', 'direction', 'horizontal');
+ });
+});
diff --git a/cypress/integration/modals.test.js b/cypress/integration/modals.test.js
new file mode 100644
index 0000000..3d2520a
--- /dev/null
+++ b/cypress/integration/modals.test.js
@@ -0,0 +1,27 @@
+///
+
+describe('Modals pop-up when header btns are pressed', () => {
+ beforeEach(() => {
+ cy.init();
+ });
+
+ // Selectors for each button
+ const ADD_LIBRARY_SEL = '[data-testid=addLibraryButton]';
+ const NEW_SEL = '[data-testid=newButton]';
+ const LOGIN_SEL = '[data-testid=loginButton]';
+
+ it('Add Library', () => {
+ cy.get(ADD_LIBRARY_SEL).click();
+ cy.get('.modal__content').should('be.visible');
+ });
+
+ it('+ New', () => {
+ cy.get(NEW_SEL).click();
+ cy.get('.modal__content').should('be.visible');
+ });
+
+ it('Login/SignUp', () => {
+ cy.get(LOGIN_SEL).click();
+ cy.get('.modal__content').should('be.visible');
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000..e2696f9
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,22 @@
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+// eslint-disable-next-line no-unused-vars
+module.exports = (on, config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+};
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000..924a863
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,31 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+Cypress.Commands.add('init', () => {
+ cy.visit('http://localhost:8080');
+
+ // closing the Welcome modal
+ cy.get('button[data-testid=closeModalButton]').click();
+});
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
diff --git a/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 0000000..37a498f
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands';
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/cypress/videos/console.test.js.mp4 b/cypress/videos/console.test.js.mp4
new file mode 100644
index 0000000..0d1e47d
Binary files /dev/null and b/cypress/videos/console.test.js.mp4 differ
diff --git a/cypress/videos/interface.test.js.mp4 b/cypress/videos/interface.test.js.mp4
new file mode 100644
index 0000000..a65756e
Binary files /dev/null and b/cypress/videos/interface.test.js.mp4 differ
diff --git a/cypress/videos/layouts.test.js.mp4 b/cypress/videos/layouts.test.js.mp4
new file mode 100644
index 0000000..2da5e8e
Binary files /dev/null and b/cypress/videos/layouts.test.js.mp4 differ
diff --git a/cypress/videos/modals.test.js.mp4 b/cypress/videos/modals.test.js.mp4
new file mode 100644
index 0000000..561ee6e
Binary files /dev/null and b/cypress/videos/modals.test.js.mp4 differ
diff --git a/package.json b/package.json
index 7ae1064..efd566f 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,9 @@
"add-locale": "lingui add-locale",
"extract": "lingui extract",
"compile": "lingui compile",
- "release:dev": "gulp devRelease"
+ "release:dev": "gulp devRelease",
+ "cypress:open": "cypress open",
+ "cypress": "cypress run"
},
"eslintConfig": {
"extends": "eslint-config-synacor"
@@ -42,6 +44,7 @@
"babel-minify": "^0.2.0",
"babel-plugin-macros": "^2.6.1",
"concurrently": "^7.0.0",
+ "cypress": "^9.5.3",
"eslint": "^4.9.0",
"eslint-config-prettier": "^2.3.0",
"eslint-config-synacor": "^2.0.2",
diff --git a/src/components/Console.jsx b/src/components/Console.jsx
index 6929d25..85d5172 100644
--- a/src/components/Console.jsx
+++ b/src/components/Console.jsx
@@ -119,6 +119,7 @@ export class Console extends PureComponent {
class="code-wrap__header-btn code-wrap__collapse-btn"
title={i18n._(t`Toggle console`)}
onClick={toggleConsole}
+ data-testid="toggleConsole"
/>
@@ -127,6 +128,7 @@ export class Console extends PureComponent {
ref={el => {
this.logContainerEl = el;
}}
+ data-testid="consoleItems"
>
{logs.map(log => (
diff --git a/src/components/CreateNewModal.jsx b/src/components/CreateNewModal.jsx
index 1bd5aab..f15c0e4 100644
--- a/src/components/CreateNewModal.jsx
+++ b/src/components/CreateNewModal.jsx
@@ -159,6 +159,7 @@ export class CreateNewModal extends Component {
trackEvent('ui', 'startBlankBtnClick');
onBlankTemplateSelect();
}}
+ data-testid="startBlankButton"
>
Start Blank
diff --git a/src/components/MainHeader.jsx b/src/components/MainHeader.jsx
index 2f9329e..9742fbf 100644
--- a/src/components/MainHeader.jsx
+++ b/src/components/MainHeader.jsx
@@ -43,6 +43,7 @@ export function MainHeader(props) {
onClick={props.addLibraryBtnHandler}
data-event-category="ui"
data-event-action="addLibraryButtonClick"
+ data-testid="addLibraryButton"
class="btn btn--dark hint--rounded hint--bottom-left"
aria-label={i18n._(t`Add a JS/CSS library`)}
>
@@ -62,6 +63,7 @@ export function MainHeader(props) {