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) {