diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ae20a9c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eslint.enable": false +} diff --git a/app/package.json b/app/package.json index 09bd96e8..bb1e80e3 100644 --- a/app/package.json +++ b/app/package.json @@ -15,6 +15,7 @@ "build": "tsc", "build:watch": "tsc -w", "start": "concurrently \"yarn build:watch\" && \"yarn electron-only\"", + "start:server": "cross-env PORT=5010 node ./build/server/start", "dev": "./node_modules/.bin/nodemon", "pack": "./node_modules/.bin/electron-builder --dir", "build:installers": "rm -rf ./dist && ./node_modules/.bin/electron-builder --publish never", diff --git a/app/server/index.ts b/app/server/index.ts index d580c819..08e8483c 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -8,7 +8,7 @@ import config from "../shared/config"; export async function startServer() { return new Promise(async (res, rej) => { try { - const port = config.port || process.env.PORT || 5010; + const port = process.env.PORT || config.port || 5010; const server = createServer(app); diff --git a/app/server/start.ts b/app/server/start.ts new file mode 100644 index 00000000..d7dab4d4 --- /dev/null +++ b/app/server/start.ts @@ -0,0 +1,3 @@ +import {startServer} from "./index" + +startServer(); \ No newline at end of file diff --git a/app/tsconfig.json b/app/tsconfig.json index 3dbb7089..920a035a 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -7,7 +7,7 @@ "sourceMap": true, "outDir": "build", "allowJs": true, - "typeRoots": ["./node_modules/@types", "types"] + "typeRoots": ["./node_modules/@types", "types", "./server/types/"] }, "include": ["server/**/*", "electron/**/*", "shared/**/*"], "exclude": ["node_modules", "__tests__", "./server/__tests__"] diff --git a/app/server/functions.js b/dummy-file.js similarity index 82% rename from app/server/functions.js rename to dummy-file.js index be848d98..d4914c83 100644 --- a/app/server/functions.js +++ b/dummy-file.js @@ -1,7 +1,7 @@ (function() { let count = 0; const interval = setInterval(() => { - console.log("hehe", count++); + console.log("test line: ", count++); }, 1000); setTimeout(() => { diff --git a/gulpfile.js b/gulpfile.js index 1d61f949..712af4d0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -10,6 +10,10 @@ const startUIForElectron = task("yarn start:electron", { cwd: path.join(__dirname, "ui") }); +const startUIForBrowser = task("yarn start:browser", { + cwd: path.join(__dirname, "ui") +}); + const startBuildWatchForApp = task("yarn build:watch", { cwd: path.join(__dirname, "app") }); @@ -22,11 +26,15 @@ const startAppsForDev = task("yarn dev", { cwd: path.join(__dirname, "app") }); +const startServerForBrowser = task("yarn start:server", { + cwd: path.join(__dirname, "app") +}); + exports.startUIForElectron = startUIForElectron; +exports.startUIForBrowser = startUIForBrowser; exports.startBuildWatchForApp = startBuildWatchForApp; exports.startAppsForDev = startAppsForDev; - /* BUILDING TASKS */ const buildUIForBrowser = task("yarn build:browser", { @@ -133,6 +141,10 @@ exports.startDesktop = series( parallel(startUIForElectron, startBuildWatchForApp, startAppsForDev) ); +exports.startBrowser = series( + parallel(startUIForBrowser, startBuildWatchForApp, startServerForBrowser) +); + exports.startCLI = series(startBuildWatchForCLI); /* BUILD TASKS */ diff --git a/package.json b/package.json index 798f09f4..9020a080 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build:desktop-azure": "./node_modules/.bin/gulp buildDesktopAzure", "build:cli": "./node_modules/.bin/gulp buildCLI", "start:desktop": "./node_modules/.bin/gulp startDesktop", + "start:browser": "./node_modules/.bin/gulp startBrowser", "start:cli": "./node_modules/.bin/gulp startCLI" }, "tenHands": { diff --git a/ui/cypress.json b/ui/cypress.json new file mode 100644 index 00000000..17ef242e --- /dev/null +++ b/ui/cypress.json @@ -0,0 +1,3 @@ +{ + "baseUrl": "http://localhost:3000" +} diff --git a/ui/cypress/fixtures/example.json b/ui/cypress/fixtures/example.json new file mode 100644 index 00000000..da18d935 --- /dev/null +++ b/ui/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/ui/cypress/integration/index.spec.js b/ui/cypress/integration/index.spec.js new file mode 100644 index 00000000..9d1f8809 --- /dev/null +++ b/ui/cypress/integration/index.spec.js @@ -0,0 +1,125 @@ +describe("Tests App top-most ui", () => { + before(() => { + cy.visit("/"); + }); + + it("Checks if topbar loaded", () => { + cy.get(".bp3-fixed-top > .bp3-align-left > .bp3-navbar-heading").should( + "have.text", + "Ten Hands" + ); + }); + + it("Checks if theme button is working", () => { + cy.getByTestId("theme-light").click(); + cy.wait(2000); + cy.getByTestId("theme-dark").click(); + }); + + it("Checks if new project button works", () => { + cy.getByTestId("new-project-button").then(subject => { + subject.click(); + cy.wait(1000); + cy.get(".bp3-drawer").then(subject => { + cy.getByText("Add Project", { container: subject }).should("exist"); + cy.getByText("Project Name", { container: subject }).should("exist"); + cy.getByText("Project Path", { container: subject }).should("exist"); + cy.getByText("Project Type", { container: subject }).should("exist"); + cy.getByText("Tasks", { container: subject }).should("exist"); + cy.getByText("Save Project", { container: subject }).should("exist"); + }); + }); + }); +}); + +describe("Checks Project", () => { + before(() => { + cy.visit("/"); + // I added project folder of 'ten-hands'. If you have cloned this project add one before this test case. + cy.get(".Pane1").then(sidebar => { + cy.getByText("ten-hands", { container: sidebar }) + .click() + .wait(2000); + }); + }); + + it("Checks ten-hands project exists", () => { + cy.getByText("New Task") + .should("exist") + .getByText("build:desktop") + .should("exist") + .getByText("yarn build:desktop") + .should("exist"); + }); + + it("Checks New Task UI", () => { + cy.getByText("New Task") + .click() + .get(".bp3-drawer") + .then(drawer => { + cy.getByText("Add Task", { container: drawer }).should("exist"); + cy.getByText("Name", { container: drawer }).should("exist"); + cy.getByText("Path", { container: drawer }).should("exist"); + cy.getByText("Task", { container: drawer }).should("exist"); + cy.getByText("Save Task", { container: drawer }).should("exist"); + }); + }); + + it("Adds a task", () => { + cy.getByLabelText(/name/i).type("Test Task"); + cy.getByLabelText(/task/i).type("node dummy-file.js"); + cy.getByTestId("save-task-button").click(); + cy.wait(2000); + cy.getByText("Test Task").should("exist"); + cy.getByText("node dummy-file.js").should("exist"); + cy.getByText("node dummy-file.js") + .scrollIntoView({ easing: "linear" }) + .should("be.visible"); + }); + + it("Runs a task", () => { + cy.getByText("Test Task") + .scrollIntoView({ easing: "linear" }) + .closest(".bp3-card") + .then(card => { + cy.getByTestId(/stop-task-button/i, { container: card }).should( + "be.disabled" + ); + cy.getByTestId(/start-task-button/i, { container: card }).click(); + cy.getByTestId(/stop-task-button/i, { container: card }).should( + "not.be.disabled" + ); + cy.wait(5000); // By that time dummy task is disabled + cy.getByTestId(/start-task-button/i, { container: card }).should( + "not.be.disabled" + ); + cy.getByTestId(/stop-task-button/i, { container: card }).should( + "be.disabled" + ); + }); + }); + + after(() => { + // Delete the added task in the end + + cy.getByText("node dummy-file.js") + .scrollIntoView({ easing: "linear" }) + .should("be.visible"); + + cy.getByText("Test Task") + .closest(".bp3-card") + .then(card => { + cy.getByTitle("Delete Task", { container: card }); + }) + .click() + .wait(1000); + + cy.getByText(/Are you sure you want to delete/gi).should("exist"); + + cy.getByText("Yes, Delete") + .click() + .wait(2000); + + cy.queryByText("node dummy-file.js").should("not.exist"); + }); +}); diff --git a/ui/cypress/plugins/index.js b/ui/cypress/plugins/index.js new file mode 100644 index 00000000..fd170fba --- /dev/null +++ b/ui/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// 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) + +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/ui/cypress/support/commands.js b/ui/cypress/support/commands.js new file mode 100644 index 00000000..c1f5a772 --- /dev/null +++ b/ui/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// 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) => { ... }) +// +// +// -- 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 is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/ui/cypress/support/index.js b/ui/cypress/support/index.js new file mode 100644 index 00000000..94c47d06 --- /dev/null +++ b/ui/cypress/support/index.js @@ -0,0 +1,22 @@ +// *********************************************************** +// 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"; + +import "@testing-library/cypress/add-commands"; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/ui/package.json b/ui/package.json index c982c92f..46c92037 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,7 +9,8 @@ "start:browser": "react-scripts start", "build:electron": "rescripts build", "build:browser": "react-scripts build", - "test": "rescripts test" + "test": "rescripts test", + "cypress:open": "./node_modules/.bin/cypress open" }, "author": { "name": "Sai Sandeep Vaddi", @@ -48,6 +49,7 @@ "@blueprintjs/tslint-config": "^1.8.0", "@rescripts/cli": "^0.0.10", "@rescripts/rescript-env": "^0.0.10", + "@testing-library/cypress": "^4.1.1", "@testing-library/dom": "^5.2.0", "@testing-library/react": "^8.0.7", "@types/react": "^16.8.17", @@ -55,6 +57,8 @@ "@types/react-dom": "^16.8.4", "@types/styled-components": "^4.1.15", "cross-env": "^5.2.0", + "cypress": "^3.4.1", + "eslint-plugin-cypress": "^2.6.1", "jest-dom": "^3.2.2", "tslint": "^5.16.0", "tslint-react-hooks": "^2.1.0", diff --git a/ui/src/components/App/AppLayout.tsx b/ui/src/components/App/AppLayout.tsx index 29584ab6..368ed7bf 100644 --- a/ui/src/components/App/AppLayout.tsx +++ b/ui/src/components/App/AppLayout.tsx @@ -14,6 +14,7 @@ import DesktopMenu from "./DesktopMenu"; const AppLayout = React.memo(() => { const { config } = useConfig(); const topbarHeight = isRunningInElectron() ? "30px" : "50px"; + console.log(isRunningInElectron()); useEffect(() => { try { const socket = io(`http://localhost:${config.port}`); diff --git a/ui/src/components/App/DesktopMenu.tsx b/ui/src/components/App/DesktopMenu.tsx index c07282ca..98a932f5 100644 --- a/ui/src/components/App/DesktopMenu.tsx +++ b/ui/src/components/App/DesktopMenu.tsx @@ -1,7 +1,7 @@ import { Button, Classes, Icon } from "@blueprintjs/core"; -import { ipcRenderer, remote } from "electron"; import React from "react"; import styled from "styled-components"; +import { isRunningInElectron } from "../../utils/electron"; import { useTheme } from "../shared/Themes"; const MenuContainer = styled.div` @@ -57,17 +57,20 @@ const MenuContainer = styled.div` `; type TMinMaxIconType = "duplicate" | "square"; -const currentWindow = remote.getCurrentWindow(); -const startingIcon: TMinMaxIconType = currentWindow.isMaximized() ? "duplicate" : "square"; - -const openAppMenu = e => { - ipcRenderer.send(`display-app-menu`, { - x: e.x, - y: e.y, - }); -}; const DesktopMenu = () => { + // Importing electron here so that code doesn't give compilation error when running in browser + const { remote, ipcRenderer } = require("electron"); + const currentWindow = remote.getCurrentWindow(); + const startingIcon: TMinMaxIconType = currentWindow.isMaximized() ? "duplicate" : "square"; + + const openAppMenu = e => { + ipcRenderer.send(`display-app-menu`, { + x: e.x, + y: e.y, + }); + }; + const { theme, setTheme } = useTheme(); const [maximizeIcon, setMaximizeIcon] = React.useState(startingIcon); const [isCloseButtonMinimal, setIsCloseButtonMinimal] = React.useState(true); diff --git a/ui/src/components/Command/Command.tsx b/ui/src/components/Command/Command.tsx index 110c4566..465bc8d0 100644 --- a/ui/src/components/Command/Command.tsx +++ b/ui/src/components/Command/Command.tsx @@ -130,7 +130,7 @@ const Command: React.FC = React.memo(({ command, socket, projectP
{command.name}