├── .DS_Store
├── .gitignore
├── LICENSE
├── README.md
├── my-app
├── .eslintrc.json
├── .gitignore
├── forge.config.ts
├── icons
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── logo.png
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
│ ├── Routes
│ │ ├── Home.tsx
│ │ └── MainPage.tsx
│ ├── UserTests
│ │ ├── TestBlock.cy.js
│ │ └── UserTestFile.cy.js
│ ├── components
│ │ ├── App.tsx
│ │ ├── ButtonComponent.tsx
│ │ ├── DescribePage.tsx
│ │ ├── DropdownButton.tsx
│ │ ├── DynamicModal.tsx
│ │ ├── Flow Components
│ │ │ ├── CustomNode.tsx
│ │ │ ├── Flow.tsx
│ │ │ ├── HtmlCustomNode.tsx
│ │ │ └── HtmlFlow.tsx
│ │ ├── FormWrapper.tsx
│ │ ├── GetFile.tsx
│ │ ├── ItBlockPage.tsx
│ │ ├── PreviewPopup.tsx
│ │ ├── SelectAction.tsx
│ │ ├── SmallerPreviewPopup.tsx
│ │ ├── StatementPage.tsx
│ │ ├── TestGenContainer.tsx
│ │ ├── TestingUi.tsx
│ │ ├── Webview.tsx
│ │ └── useMultiStepForm.ts
│ ├── emptyFile.cy.js
│ ├── getNonce.ts
│ ├── index.css
│ ├── index.html
│ ├── index.ts
│ ├── main.tsx
│ ├── options
│ │ ├── actionOptions.ts
│ │ ├── assertionOptions.ts
│ │ ├── optionVariables.ts
│ │ └── otherCommandOptions.ts
│ ├── parser.ts
│ ├── preload.ts
│ ├── renderer.ts
│ └── types
│ │ ├── ImportObj.ts
│ │ └── Tree.ts
├── tailwind.config.js
├── testing
│ └── test2.js
├── tsconfig.json
├── webpack.main.config.ts
├── webpack.plugins.ts
├── webpack.renderer.config.ts
└── webpack.rules.ts
├── package-lock.json
└── package.json
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Cydekick/efd8edf339389abdc3687c350bf4aa6b704fa6a7/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Preston Mounivong
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Cydekick.com
24 |
25 |
26 | Cydekick is a Cypress test code generator designed for React applications. It enables you to visualize the component hierarchy of your React application, interact with your application in real-time, and seamlessly generate test code.
27 |
28 | ## Table of Contents
29 | - [Interface & Features](#interface--features)
30 | - [Prerequisites](#prerequisites)
31 | - [Installation](#installation)
32 | - [Usage](#usage)
33 | - [Contributing](#contributing)
34 | - [Support](#support)
35 | - [For Future Iterations](#future-iterations)
36 | - [License](#license-information)
37 | - [The Cydekick Team](#the-cydekick-team)
38 |
39 | ## Interface & Features
40 |
41 | Overview:
42 | Upon launching Cydekick, users are presented with an intuitive interface designed to simplify Cypress testing for React applications. The main features include:
43 |
44 | 1. Visual Component Hierarchy:
45 | - Navigate through the component hierarchy using React Flow, providing a visual representation of your React application's structure.
46 |
47 | 2. Highlight Components:
48 | - Click on a component to reveal its HTML structure, allowing users to inspect and interact with individual components.
49 |
50 | 3. Generate Test Code:
51 | - Seamlessly generate Cypress test code by providing text for "describe" and "it" blocks, along with selecting Cypress commands for testing.
52 |
53 | 4. Preview and Save:
54 | - Preview and save the generated test code in a separate file, giving users the flexibility to edit and download the code at their convenience.
55 |
56 | ## Prerequisites
57 |
58 | To use Cydekick, you will need to:
59 |
60 | - Install Cypress in your application.
61 | - Add your own "data-cy" IDs to elements you wish to test on your app.
62 | - Run your application concurrently.
63 |
64 | ## Installation
65 |
66 | Follow these steps to install Cydekick:
67 |
68 | 1. Download the latest version of Cydekick from [HERE](https://cydekick.dev/#home).
69 | 2. Choose the version that matches your operating system: Mac, Windows, or Linux.
70 | 3. Unzip the file onto your Computer and proceed to the next steps below.
71 |
72 | ## Usage
73 |
74 | To use Cydekick effectively, follow these steps:
75 |
76 | 1. Launch Cydekick.
77 |
78 | 2. Input the files of your React application and the URL where it's hosted.
79 |
80 |
81 |
82 |
83 |
84 | 3. Explore the component hierarchy using React Flow and select the component of your application that you want to test.
85 |
86 |
87 |
88 |
89 |
90 | 4. Generate test code by providing text for "describe" and "it" blocks, and selecting Cypress commands.
91 |
92 |
93 |
94 |
95 |
96 | 5. Ensure you add the statement to the editor by clicking the "end statement" button.
97 |
98 | 6. Complete the "describe" block and click "preview" at the top right to view your test file.
99 |
100 | 7. Congratulations on your first test code; You're free to edit, preview, or save the generated code.
101 |
102 |
103 |
104 |
105 |
106 | ## Contributing
107 |
108 |
109 | We've launched Cydekick as a valuable tool to streamline Cypress testing for users. We plan to introduce more features, extensions, and enhancements to make Cypress testing even more efficient. We appreciate any contributions from the community and encourage you to give Cydekick a try. Feel free to suggest improvements or report any issues you encounter using the application. Your interest and involvement are highly valued!
110 |
111 | ## For Future Iterations
112 |
113 | - Currently, the Cypress commands are not compatible with anonymous functions.
114 | - The commands do not support an "options" object parameter.
115 | - The commands only support strings and numbers.
116 | - There is a limited number of command options.
117 |
118 | ## Support
119 |
120 | Encounter issues or have suggestions? Please feel free to message the creators down below.
121 |
122 | ## License Information
123 |
124 | Cydekick is licensed under the MIT License. See the [LICENSE](https://github.com/oslabs-beta/Cydekick/blob/main/LICENSE) file for details.
125 |
126 | ## The Cydekick Team
127 |
128 | Developed by:
129 |
130 | - **Preston Mounivong**
131 | - LinkedIn: [Preston Mounivong](https://www.linkedin.com/in/prestonmounivong/)
132 | - GitHub: [prrrrreston](https://github.com/prrrrreston)
133 |
134 | - **Sid Saxena**
135 | - LinkedIn: [Sid Saxena](https://www.linkedin.com/in/siddhantsaxena27/)
136 | - GitHub: [sidsaxena27](https://github.com/sidsaxena27)
137 |
138 | - **Jacob Sasser**
139 | - LinkedIn: [Jacob Sasser](https://www.linkedin.com/in/jacob-sasser-11a424112/)
140 | - GitHub: [jacobsasser](https://github.com/jacobsasser)
141 |
142 | - **Quinn Craddock**
143 | - LinkedIn: [Quinn Craddock](https://www.linkedin.com/in/quinn-craddock4/)
144 | - GitHub: [quinnCraddock4](https://github.com/quinnCraddock4)
145 |
146 |
--------------------------------------------------------------------------------
/my-app/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "plugin:import/recommended",
12 | "plugin:import/electron",
13 | "plugin:import/typescript"
14 | ],
15 | "parser": "@typescript-eslint/parser"
16 | }
17 |
--------------------------------------------------------------------------------
/my-app/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 | .DS_Store
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # TypeScript cache
43 | *.tsbuildinfo
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 | .env.test
63 |
64 | # parcel-bundler cache (https://parceljs.org/)
65 | .cache
66 |
67 | # next.js build output
68 | .next
69 |
70 | # nuxt.js build output
71 | .nuxt
72 |
73 | # vuepress build output
74 | .vuepress/dist
75 |
76 | # Serverless directories
77 | .serverless/
78 |
79 | # FuseBox cache
80 | .fusebox/
81 |
82 | # DynamoDB Local files
83 | .dynamodb/
84 |
85 | # Webpack
86 | .webpack/
87 |
88 | # Vite
89 | .vite/
90 |
91 | # Electron-Forge
92 | out/
93 |
--------------------------------------------------------------------------------
/my-app/forge.config.ts:
--------------------------------------------------------------------------------
1 | import type { ForgeConfig } from '@electron-forge/shared-types';
2 | import { MakerSquirrel } from '@electron-forge/maker-squirrel';
3 | import { MakerZIP } from '@electron-forge/maker-zip';
4 | import { MakerDeb } from '@electron-forge/maker-deb';
5 | import { MakerRpm } from '@electron-forge/maker-rpm';
6 | import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
7 | import { WebpackPlugin } from '@electron-forge/plugin-webpack';
8 |
9 | import { mainConfig } from './webpack.main.config';
10 | import { rendererConfig } from './webpack.renderer.config';
11 |
12 | const config: ForgeConfig = {
13 | packagerConfig: {
14 | asar: {
15 | unpackDir: "src/UserTests"
16 | },
17 | icon: './icons/icon', // based on platform, will "add" .png for linux, .ico for windows, and .icns for mac
18 | },
19 | rebuildConfig: {},
20 | makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerRpm({}), new MakerDeb({})],
21 | plugins: [
22 | new AutoUnpackNativesPlugin({}),
23 | new WebpackPlugin({
24 | mainConfig,
25 | renderer: {
26 | config: rendererConfig,
27 | entryPoints: [
28 | {
29 | html: './src/index.html',
30 | js: './src/renderer.ts',
31 | name: 'main_window',
32 | preload: {
33 | js: './src/preload.ts',
34 | },
35 | },
36 | ],
37 | },
38 | }),
39 | ],
40 | };
41 |
42 | export default config;
43 |
--------------------------------------------------------------------------------
/my-app/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Cydekick/efd8edf339389abdc3687c350bf4aa6b704fa6a7/my-app/icons/icon.icns
--------------------------------------------------------------------------------
/my-app/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Cydekick/efd8edf339389abdc3687c350bf4aa6b704fa6a7/my-app/icons/icon.ico
--------------------------------------------------------------------------------
/my-app/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Cydekick/efd8edf339389abdc3687c350bf4aa6b704fa6a7/my-app/icons/icon.png
--------------------------------------------------------------------------------
/my-app/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Cydekick/efd8edf339389abdc3687c350bf4aa6b704fa6a7/my-app/logo.png
--------------------------------------------------------------------------------
/my-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Cydekick",
3 | "productName": "Cydekick",
4 | "version": "1.0.0",
5 | "description": "My Electron application description",
6 | "main": ".webpack/main",
7 | "scripts": {
8 | "start": "electron-forge start",
9 | "package": "electron-forge package",
10 | "make": "electron-forge make",
11 | "make:linux": "electron-forge make --platform=linux",
12 | "make:win": "electron-forge make --platform=win32",
13 | "make:mac": "electron-forge make --platform=darwin",
14 | "publish": "electron-forge publish",
15 | "lint": "eslint --ext .ts,.tsx ."
16 | },
17 | "keywords": [],
18 | "author": {
19 | "name": "Siddhant Saxena",
20 | "email": "sidsaxena27@gmail.com"
21 | },
22 | "license": "MIT",
23 | "devDependencies": {
24 | "@babel/core": "^7.22.20",
25 | "@babel/parser": "^7.22.16",
26 | "@babel/preset-react": "^7.22.15",
27 | "@electron-forge/cli": "^6.4.2",
28 | "@electron-forge/maker-deb": "^6.4.2",
29 | "@electron-forge/maker-rpm": "^6.4.2",
30 | "@electron-forge/maker-squirrel": "^6.4.2",
31 | "@electron-forge/maker-zip": "^6.4.2",
32 | "@electron-forge/plugin-auto-unpack-natives": "^6.4.2",
33 | "@electron-forge/plugin-webpack": "^6.4.2",
34 | "@types/react": "^18.2.21",
35 | "@types/react-dom": "^18.2.7",
36 | "@typescript-eslint/eslint-plugin": "^5.62.0",
37 | "@typescript-eslint/parser": "^5.62.0",
38 | "@vercel/webpack-asset-relocator-loader": "^1.7.3",
39 | "babel-loader": "^9.1.3",
40 | "css-loader": "^6.8.1",
41 | "electron": "26.2.1",
42 | "eslint": "^8.49.0",
43 | "eslint-plugin-import": "^2.28.1",
44 | "fork-ts-checker-webpack-plugin": "^7.3.0",
45 | "node-loader": "^2.0.0",
46 | "postcss-loader": "^7.3.3",
47 | "postcss-nesting": "^12.0.1",
48 | "style-loader": "^3.3.3",
49 | "tailwindcss": "^3.3.3",
50 | "ts-loader": "^9.4.4",
51 | "ts-node": "^10.9.1",
52 | "typescript": "~4.5.4"
53 | },
54 | "dependencies": {
55 | "@dagrejs/dagre": "^1.0.4",
56 | "@electron/remote": "^2.0.11",
57 | "@types/node": "^20.7.0",
58 | "babel-plugin-react-css-modules": "^5.2.6",
59 | "electron-squirrel-startup": "^1.0.0",
60 | "fs": "^0.0.1-security",
61 | "monaco-editor": "^0.39.0",
62 | "monaco-editor-webpack-plugin": "^7.1.0",
63 | "path": "^0.12.7",
64 | "react": "^18.2.0",
65 | "react-dom": "^18.2.0",
66 | "react-flow-renderer": "^10.3.17",
67 | "react-monaco-editor": "^0.54.0",
68 | "react-router": "^6.16.0",
69 | "react-router-dom": "^6.16.0",
70 | "util": "^0.12.5"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/my-app/postcss.config.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: "off" */
2 | const tailwindcss = require('tailwindcss');
3 |
4 | module.exports = {
5 | plugins: [
6 | tailwindcss('./tailwind.config.js')],
7 | };
8 |
--------------------------------------------------------------------------------
/my-app/src/Routes/Home.tsx:
--------------------------------------------------------------------------------
1 | import GetFile from "../components/GetFile";
2 | import React from "react";
3 | import { Tree } from "../types/Tree";
4 |
5 | /*
6 | mint greenish: #1DF28F
7 | darker green: #048C7F
8 | background color: #1B1E26
9 |
10 | */
11 | type HomePageProps = {
12 | setUrl: React.Dispatch>;
13 | url: string;
14 | fileTree:Tree;
15 | setPageState: React.Dispatch>;
16 | setFileTree: React.Dispatch>;
17 | };
18 |
19 | const Home = (props: HomePageProps) => {
20 | const { fileTree, setUrl, setFileTree, url, setPageState } = props;
21 | const [buttonSlide, setButtonSlide] = React.useState(false);
22 | // Button Handler to switch Routes
23 | const handleSubmission = () => {
24 | setButtonSlide(true)
25 | setTimeout(()=>{
26 | setPageState("MainPage");
27 | setButtonSlide(false)
28 | }, 300);
29 | }
30 |
31 | const handleChange = () => {
32 | const urlInputElement = document.getElementById(
33 | "url_form_id"
34 | ) as HTMLInputElement;
35 | if (urlInputElement) {
36 | setUrl(urlInputElement.value);
37 | }
38 | }
39 |
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
55 |
66 | Next
67 |
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default Home;
76 |
--------------------------------------------------------------------------------
/my-app/src/Routes/MainPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Webview from '../components/Webview';
3 | import Flow from '../components/Flow Components/Flow';
4 | import ButtonComponent from '../components/ButtonComponent';
5 | import TestGenContainer from '../components/TestGenContainer';
6 | import { Tree as TreeType } from '../types/Tree';
7 | import HtmlFlow from '../components/Flow Components/HtmlFlow';
8 |
9 | type MainPageProps = {
10 | url: string;
11 | fileTree: TreeType;
12 | setPageState: React.Dispatch>;
13 | };
14 |
15 | const MainPage = (props: MainPageProps) => {
16 | const { url, fileTree, setPageState } = props;
17 | const [currentComponent, setCurrentComponent] = React.useState({
18 | id: '',
19 | name: '',
20 | fileName: '',
21 | filePath: '',
22 | importPath: '',
23 | expanded: false,
24 | depth: 0,
25 | count: 0,
26 | thirdParty: false,
27 | reactRouter: false,
28 | reduxConnect: false,
29 | children: [],
30 | htmlChildrenTestIds: {},
31 | parentList: [],
32 | props: {},
33 | error: '',
34 | });
35 | const [currentHTML, setCurrentHTML] = React.useState('');
36 | const [currentTestId, setCurrentTestId] = React.useState('');
37 | const [data, setData] = React.useState('');
38 | const [onComponentFlow, setOnComponentFlow] = React.useState(true);
39 |
40 | React.useEffect(() => console.log(currentTestId), [currentTestId]);
41 | // Route Handling between pages
42 | const handleBack = () => {
43 | setPageState('Home');
44 | };
45 | const flowToggle = () => {
46 | if (data) setOnComponentFlow(!onComponentFlow);
47 | };
48 | const handleReload = () => {
49 | const webview = document.getElementById(
50 | "webview"
51 | ) as Electron.WebviewTag | null;
52 | webview.loadURL(url);
53 | };
54 |
55 | return (
56 |
57 |
61 |
65 | Back
66 |
67 |
71 | Reload URL
72 |
73 |
74 |
75 |
76 |
83 |
92 |
98 |
99 |
100 |
105 |
106 |
107 | );
108 | };
109 |
110 | export default MainPage;
111 |
--------------------------------------------------------------------------------
/my-app/src/UserTests/TestBlock.cy.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Cydekick/efd8edf339389abdc3687c350bf4aa6b704fa6a7/my-app/src/UserTests/TestBlock.cy.js
--------------------------------------------------------------------------------
/my-app/src/UserTests/UserTestFile.cy.js:
--------------------------------------------------------------------------------
1 | describe('fd', () => {
2 | it('sd', () => {
3 | .clear()})
4 | it('sdf', () => {
5 | })
6 | })
--------------------------------------------------------------------------------
/my-app/src/components/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Home from "../Routes/Home";
3 | import MainPage from "../Routes/MainPage";
4 | import {Tree} from "../types/Tree"
5 |
6 | const App = () => {
7 | const [url, setUrl] = React.useState('http://localhost:8080/');
8 | const [fileTree, setFileTree] = React.useState({
9 | id: '',
10 | name: '',
11 | fileName: '',
12 | filePath: '',
13 | importPath: '',
14 | expanded: false,
15 | depth: 0,
16 | count: 0,
17 | thirdParty: false,
18 | reactRouter: false,
19 | reduxConnect: false,
20 | children: [],
21 | htmlChildrenTestIds: {},
22 | parentList: [],
23 | props: {},
24 | error: '',
25 | });
26 | const [pageState, setPageState] = React.useState("Home");
27 |
28 | return pageState === "Home" ? (
29 |
36 | ) : (
37 |
38 | );
39 | };
40 |
41 | export default App;
42 |
--------------------------------------------------------------------------------
/my-app/src/components/ButtonComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PreviewPopup from './PreviewPopup';
3 | import path from 'path';
4 | import os from 'os';
5 | import fs from 'fs';
6 |
7 | const { ipcRenderer } = window.require('electron');
8 |
9 |
10 | const ButtonComponent = () => {
11 | const [open, setOpen] = React.useState(false);
12 |
13 | const handleOpen = () => {
14 | setOpen(true);
15 | };
16 |
17 | const handleClose = () => {
18 | setOpen(false);
19 | };
20 |
21 | // const handleSaveFile = async () => {
22 | // const tempDirPath = path.join(os.tmpdir(), 'UserTests', 'UserTestFile.cy.js');
23 | // const content = fs.readFileSync(tempDirPath, 'utf-8');
24 |
25 | // // const { dialog } = window.require('@electron/remote');
26 | // const filePath = await dialog.showSaveDialog({
27 | // title: 'Save your file',
28 | // defaultPath: 'UserTestFile.cy.js',
29 | // filters: [{ name: 'JavaScript', extensions: ['js'] }],
30 | // });
31 |
32 | // if (filePath) {
33 | // fs.writeFileSync(filePath, content);
34 | // }
35 | // };
36 | const handleSaveFile = () => {
37 | const tempDirPath = path.join(os.tmpdir(), 'UserTests', 'UserTestFile.cy.js');
38 | const content = fs.readFileSync(tempDirPath, 'utf-8');
39 |
40 | ipcRenderer.invoke('save-file', content);
41 | };
42 |
43 |
44 | return (
45 |
46 |
49 | Preview and Edit File
50 |
51 |
52 |
53 | Save File As...
54 |
55 | {open &&
}
56 |
57 | );
58 | };
59 |
60 | export default ButtonComponent;
--------------------------------------------------------------------------------
/my-app/src/components/DescribePage.tsx:
--------------------------------------------------------------------------------
1 | import SmallerPreviewPopup from './SmallerPreviewPopup'
2 | import React from 'react';
3 | const fs = window.require('fs');
4 | const os = window.require('os');
5 |
6 | const path = window.require('path')
7 |
8 | type DescribePageProps = {
9 | setCurrentPageNum: React.Dispatch>
10 | }
11 |
12 | function DescribePage({ setCurrentPageNum }: DescribePageProps) {
13 |
14 | //States
15 | const [code, setCode] = React.useState(''); // initial string that is displayed on the editor
16 |
17 | //text that is rendered whenever the describe page is mounted
18 | React.useEffect(() => {
19 | const fileContent = `'Welcome to Cydekick!' \n'Enter text for your describe block!'`
20 | setCode(fileContent);
21 | }, []);
22 |
23 | //Describe block function that appends our describe block to the monaco editor
24 | function createDescribeBlock(): void {
25 | const describeText = (document.getElementById('describeText') as HTMLInputElement).value
26 | const testFileContent = describeBlock(describeText);
27 | // const filePath = path.join(__dirname, 'src', 'UserTests', 'TestBlock.cy.js');
28 | const filePath = path.join(os.tmpdir(), 'UserTests', 'TestBlock.cy.js');
29 |
30 |
31 | fs.writeFileSync(filePath, testFileContent);
32 | setCurrentPageNum(1)
33 | }
34 |
35 | //describe block that returns a describe string
36 | function describeBlock(string: string): string {
37 | return `describe('${string}', () => {`;
38 | }
39 |
40 |
41 | return (
42 |
43 |
46 |
Name for describe block:
47 |
52 |
55 | Create describe block
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
65 | export default DescribePage
66 |
67 |
--------------------------------------------------------------------------------
/my-app/src/components/DropdownButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import DynamicModal from './DynamicModal';
3 |
4 | interface Modal {
5 | type: string;
6 | labelText?: string;
7 | inputType?: string;
8 | options?: string[]
9 | }
10 |
11 | interface OptionDetails {
12 | option: string;
13 | code: string;
14 | tooltip: string;
15 | modal?: Modal[];
16 | modalCreateCode?: (text:string[]) => string;
17 | }
18 |
19 | interface Props {
20 | label: string;
21 | options: Record;
22 | onClickOption: (code: string, details: OptionDetails) => void;
23 | dropDown: string;
24 | setDropDown: (param:string)=>void
25 | }
26 |
27 | const DropdownButton: React.FC = ({ dropDown, setDropDown, label, onClickOption, options }) => {
28 | const [isOpen, setIsOpen] = useState(false);
29 |
30 | // array to keep track of state
31 | const [modalOpenStates, setModalOpenStates] = useState(
32 | Object.keys(options).map(() => false),
33 | );
34 |
35 | function onButtonClick(index: number, optionDetails: OptionDetails) {
36 | if (!optionDetails.modal) {
37 | onClickOption(optionDetails.code, optionDetails);
38 | } else {
39 | // Open the modal for the selected option by updating its state
40 | const updatedModalStates = [...modalOpenStates];
41 | updatedModalStates[index] = true;
42 | setModalOpenStates(updatedModalStates);
43 | }
44 | }
45 |
46 | function closeModal(index: number) {
47 | // Close the modal for the specified option by updating its state
48 | const updatedModalStates = [...modalOpenStates];
49 | updatedModalStates[index] = false;
50 | setModalOpenStates(updatedModalStates);
51 | }
52 | React.useEffect(() =>{
53 | if (label !== dropDown){
54 | setIsOpen(false)
55 | }
56 | }, [dropDown])
57 | return (
58 |
59 |
60 | {' '}
61 | {/* Adjust spacing between parent buttons */}
62 | {
63 | setDropDown(label);
64 | setIsOpen(!isOpen);
65 |
66 | }}>
67 | {label}
68 |
69 | {/* Add other parent buttons here */}
70 |
71 | {isOpen && (
72 |
73 | {Object.values(options).map((optionDetails, index) => (
74 |
75 | {
77 | onButtonClick(index, optionDetails);
78 | }}
79 | title={optionDetails.tooltip}
80 | className='w-full h-full text-center mb-2 hover:bg-secondary hover:text-secondaryPrimary p-1 rounded'>
81 | {optionDetails.option}
82 |
83 | closeModal(index)}
87 | onClickOption={onClickOption}
88 | />
89 |
90 | ))}
91 |
92 | )}
93 |
94 | );
95 | };
96 |
97 | export default DropdownButton;
98 |
--------------------------------------------------------------------------------
/my-app/src/components/DynamicModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface Modal {
4 | type: string;
5 | labelText?: string;
6 | inputType?: string;
7 | options?: string[]
8 | }
9 |
10 |
11 | interface OptionDetails {
12 | option: string;
13 | code: string;
14 | tooltip: string;
15 | modal?: Modal[];
16 | modalCreateCode?: (text:string[]) => string;
17 | }
18 |
19 | type DynamicModalType = {
20 | infoObj:OptionDetails;
21 | isOpen:boolean;
22 | setIsOpen:React.Dispatch>;
23 | onClickOption:(code: string, details: OptionDetails) => void;
24 | }
25 |
26 | function DynamicModal({ infoObj, isOpen, setIsOpen, onClickOption }:DynamicModalType) {
27 | function createCode(): void {
28 | const modalForm = document.getElementById('modalForm') as HTMLFormElement | null;
29 | if (modalForm){
30 |
31 | const modalElements = modalForm.elements as HTMLFormControlsCollection;
32 | const arrayOfEleVal: any[] = [];
33 | for (const ele of modalElements) {
34 | if (ele instanceof HTMLTextAreaElement || ele instanceof HTMLSelectElement) arrayOfEleVal.push(ele.value);
35 | }
36 | onClickOption(infoObj.modalCreateCode(arrayOfEleVal), infoObj);
37 | setIsOpen(false);
38 | }
39 | }
40 |
41 | function createLabel(labelText: string) {
42 | return {labelText} ;
43 | }
44 |
45 | function createInput(inputType:string) {
46 | return ;
47 | }
48 |
49 | function createSelect(options:string[]) {
50 | const allOptions = options.map((opt) => {
51 | return {opt} ;
52 | });
53 | return {allOptions} ;
54 | }
55 |
56 | function createFormContent(arrOfForm:Modal[]) {
57 | const modalContent = arrOfForm.map((item) => {
58 | if (item.type === 'label') {
59 | return createLabel(item.labelText);
60 | } else if (item.type === 'input') {
61 | return createInput(item.inputType);
62 | } else if (item.type === 'select') {
63 | return createSelect(item.options);
64 | }
65 | });
66 |
67 | return (
68 |
69 |
70 |
71 | setIsOpen(false)}>
74 | Go Back
75 |
76 | createCode()}>
79 | Confirm
80 |
81 |
82 |
83 | );
84 | }
85 |
86 | return (
87 |
88 | {isOpen && (
89 |
90 |
91 |
92 |
93 | {createFormContent(infoObj.modal)}
94 |
95 |
96 |
97 | )}
98 |
99 | );
100 | }
101 |
102 | export default DynamicModal;
103 |
--------------------------------------------------------------------------------
/my-app/src/components/Flow Components/CustomNode.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import { Handle, Position } from "react-flow-renderer";
3 | import { Tree as TreeType } from "../../types/Tree";
4 | type CustomNodeProps = {
5 | data:{
6 | name:string;
7 | testid:string;
8 | props:{[key: string]: boolean;}
9 | filePath:string;
10 | setCurrentComponent: React.Dispatch>;
11 | nodeData:TreeType;
12 | isSelected:boolean
13 | key:string
14 | }
15 | }
16 | function CustomNode({ data }:CustomNodeProps) {
17 |
18 | const handleClick = () => {
19 | data.setCurrentComponent(data.nodeData);
20 | };
21 |
22 |
23 | return (
24 |
29 | {data.name}
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default memo(CustomNode);
37 |
--------------------------------------------------------------------------------
/my-app/src/components/Flow Components/Flow.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactFlow, {
3 | useNodesState,
4 | useEdgesState,
5 | Controls,
6 | ControlButton,
7 | } from "react-flow-renderer";
8 | import dagre from "@dagrejs/dagre";
9 | import { Tree as TreeType } from "../../types/Tree";
10 |
11 | import "react-flow-renderer/dist/style.css";
12 |
13 | import "../../../tailwind.config";
14 | import CustomNode from "./CustomNode";
15 |
16 | const nodeTypes = {
17 | custom: CustomNode,
18 | };
19 |
20 | type FlowProps = {
21 | fileTree: TreeType;
22 | currentComponent: TreeType;
23 | setCurrentComponent: React.Dispatch>;
24 | onComponentFlow: boolean;
25 | flowToggle: () => void;
26 | };
27 | type NodeType = {
28 | id:string;
29 | type:string;
30 | data:{
31 | name:string;
32 | testid:string;
33 | props:{[key: string]: boolean;}
34 | filePath:string;
35 | setCurrentComponent: React.Dispatch>;
36 | nodeData:TreeType;
37 | isSelected:boolean
38 | }
39 | position:{x:number, y:number}
40 | }
41 | type EdgeType = {
42 | id:string;
43 | target:string;
44 | source:string;
45 | }
46 |
47 | const Flow = ({
48 | flowToggle,
49 | onComponentFlow,
50 | fileTree,
51 | currentComponent,
52 | setCurrentComponent,
53 | }: FlowProps) => {
54 | const nodesArr:NodeType[] = [];
55 | const edgesArr:EdgeType[] = [];
56 |
57 | (function treeParser(tree: TreeType) {
58 | if (!tree.reactRouter && !tree.reduxConnect) {
59 | nodesArr.push({
60 | id: tree.name,
61 | type: "custom",
62 | data: {
63 | name: tree.name,
64 | testid: tree.htmlChildrenTestIds,
65 | props: tree.props,
66 | filePath: tree.filePath,
67 | setCurrentComponent: setCurrentComponent,
68 | nodeData: tree,
69 | isSelected: false,
70 | },
71 | position: { x: 0, y: 0 },
72 | });
73 | if (tree.parentList.length > 0) {
74 | // find the name of the first parent in the list
75 | // find the index in the ndoes Arr whos data.filePath === tree.parentList[0]
76 | const index = nodesArr.findIndex(
77 | (node) => node.data.filePath === tree.parentList[0]
78 | );
79 | if (index !== -1) {
80 | edgesArr.push({
81 | id: `${tree.name}-${nodesArr[index].id}`,
82 | target: tree.name,
83 | source: nodesArr[index].id,
84 | });
85 | }
86 | }
87 | }
88 | if (tree.children.length > 0)
89 | tree.children.forEach((child) => treeParser(child));
90 | })(fileTree);
91 |
92 | React.useEffect(() => {
93 | nodes.forEach((node) => {
94 | if (node.data.nodeData === currentComponent) node.data.isSelected = true;
95 | else node.data.isSelected = false;
96 | });
97 | }, [currentComponent]);
98 |
99 | const dagreGraph = new dagre.graphlib.Graph();
100 | dagreGraph.setDefaultEdgeLabel(() => ({}));
101 |
102 | const nodeWidth = 176;
103 | const nodeHeight = 36;
104 |
105 | const getLayoutedElements = (nodes:NodeType[], edges:EdgeType[], direction = "TB") => {
106 | dagreGraph.setGraph({ rankdir: direction });
107 |
108 | nodes.forEach((node) => {
109 | dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
110 | });
111 |
112 | edges.forEach((edge) => {
113 | dagreGraph.setEdge(edge.source, edge.target);
114 | });
115 |
116 | dagre.layout(dagreGraph);
117 |
118 | nodes.forEach((node) => {
119 | const nodeWithPosition = dagreGraph.node(node.id);
120 | node.position = {
121 | x: nodeWithPosition.x - nodeWidth / 2,
122 | y: nodeWithPosition.y - nodeHeight / 2,
123 | };
124 |
125 | return node;
126 | });
127 |
128 | return { nodes, edges };
129 | };
130 | const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
131 | nodesArr,
132 | edgesArr
133 | );
134 | const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
135 | const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges);
136 |
137 | return (
138 |
139 | ({
141 | ...node,
142 | data: {
143 | ...node.data,
144 | key: node.id, // Use the node's ID as the key prop
145 | },
146 | }))}
147 | edges={edges}
148 | onNodesChange={onNodesChange}
149 | onEdgesChange={onEdgesChange}
150 | nodeTypes={nodeTypes}
151 | fitView
152 | className="bg-transparent"
153 | >
154 |
155 |
159 | HTML
160 |
161 |
162 |
163 |
164 |
165 | );
166 | };
167 |
168 | export default Flow;
169 |
--------------------------------------------------------------------------------
/my-app/src/components/Flow Components/HtmlCustomNode.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Handle, Position } from "react-flow-renderer";
3 |
4 | type HtmlCustomNode = {
5 | id:string;
6 | data: {
7 | name:string;
8 | attributes:any;
9 | setCurrentHTML:React.Dispatch>;
10 | setCurrentTestId:React.Dispatch>;
11 | isSelected:boolean;
12 | }
13 | }
14 |
15 | const CustomNode = ({ id, data }:HtmlCustomNode) => {
16 |
17 | const handleClick = () =>{
18 | // if data-cy exists set current html to that data.name and set currentTestid to that testid
19 | if (data.attributes["data-cy"]){
20 | data.setCurrentHTML(data.name);
21 | data.setCurrentTestId(`data-cy = ${data.attributes["data-cy"].value}`)
22 | }
23 | else if (data.attributes["id"]){
24 | data.setCurrentHTML(data.name);
25 | data.setCurrentTestId(`id = ${data.attributes["id"].value}`)
26 | }
27 | };
28 |
29 | return (
30 |
40 |
41 | {data.name}
42 | {data.attributes["data-cy"] && (
43 | data-cy = {data.attributes["data-cy"].value}
44 | )}
45 | {data.attributes["id"] && id = {data.attributes["id"].value}
}
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default CustomNode;
54 |
--------------------------------------------------------------------------------
/my-app/src/components/Flow Components/HtmlFlow.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactFlow, { Controls, ControlButton } from "react-flow-renderer";
3 |
4 | import "react-flow-renderer/dist/style.css";
5 |
6 | import "../../../tailwind.config";
7 | import CustomNode from "./HtmlCustomNode";
8 |
9 | const nodeTypes = {
10 | custom: CustomNode,
11 | };
12 |
13 | type HtmlFlowProps = {
14 | flowToggle:()=>void;
15 | onComponentFlow:boolean;
16 | data:string;
17 | currentHTML:string;
18 | setCurrentHTML:React.Dispatch>;
19 | currentTestId:string;
20 | setCurrentTestId:React.Dispatch>;
21 | };
22 |
23 | type NodeType = {
24 | id:string;
25 | type:string;
26 | data: {
27 | name:string;
28 | attributes:any;
29 | setCurrentHTML:React.Dispatch>;
30 | setCurrentTestId:React.Dispatch>;
31 | isSelected:boolean;
32 | }
33 | position: {x:number, y:number}
34 | }
35 |
36 | type EdgeType = {
37 | id:string;
38 | target:string;
39 | source:string;
40 | }
41 |
42 | const HtmlFlow = ({ flowToggle, onComponentFlow, data, currentHTML, setCurrentHTML, setCurrentTestId, currentTestId }:HtmlFlowProps) => {
43 |
44 | const [edges, setEdges] = React.useState([]);
45 | const [nodes, setNodes] = React.useState([]);
46 |
47 | React.useEffect(() => {
48 | const parsedHtml = new DOMParser().parseFromString(data, "text/html");
49 | let counter = 0;
50 | const nodesArr:NodeType[] = [];
51 | const edgesArr:EdgeType[] = [];
52 | let maxDepth = 0;
53 |
54 | const createNodeFromElement = (
55 | element:any,
56 | depth: number,
57 | parent: any = null
58 | ) => {
59 | if (depth > maxDepth) maxDepth = depth;
60 | const node = {
61 | id: `node-${counter++}`,
62 | type: "custom",
63 | data: {
64 | name: element.nodeName,
65 | attributes: element.attributes,
66 | setCurrentHTML: setCurrentHTML,
67 | setCurrentTestId: setCurrentTestId,
68 | isSelected: false
69 | },
70 | position: { x: 0, y: depth * 150 },
71 | };
72 | if (parent) {
73 | const edge = {
74 | id: `${parent.id}-${node.id}`,
75 | source: parent.id,
76 | target: node.id,
77 | };
78 | edgesArr.push(edge);
79 | }
80 | nodesArr.push(node);
81 | return node;
82 | };
83 |
84 | const connectParentToChildren = (
85 | element:any,
86 | depth: number,
87 | parent: any = null
88 | ) => {
89 | const node = createNodeFromElement(element, depth, parent);
90 | const children = Array.from(element.children);
91 | children.forEach((childElement) => {
92 | // Recursively connect the child's children
93 | connectParentToChildren(childElement, depth + 1, node);
94 | });
95 | };
96 |
97 | connectParentToChildren(parsedHtml.body, 0);
98 | // depth is correct, need to fix horizonatl positioning of the nodes arr.
99 | const newNodesArr = [];
100 | for (let i = 0; i <= maxDepth; i++) {
101 | // grab all nodes on the same y level;
102 | const temp:NodeType[] = nodesArr.filter((el) => el.position.y === i * 150);
103 | if (temp.length !== 0) {
104 | const totalWidth = temp.length * 300; // Assuming node width is 176px
105 | // Calculate the initial x position for the first node
106 | const initialX = -totalWidth / 2;
107 | // Calculate the spacing between nodes
108 | const spacingX = 300; // Assuming node width is 176px
109 | temp.forEach((node, index) => {
110 | const newX = initialX + index * spacingX;
111 | node.position.x = newX;
112 | });
113 | newNodesArr.push(...temp);
114 | }
115 | }
116 | setNodes(newNodesArr);
117 | setEdges(edgesArr);
118 | }, [data]);
119 | React.useEffect(() => {
120 | const temp:NodeType[] = nodes.map((node) => ({
121 | ...node,
122 | data: {
123 | ...node.data,
124 | isSelected: (
125 | currentHTML === node.data.name &&
126 | (
127 | (node.data.attributes["id"] && currentTestId === `id = ${node.data.attributes["id"].value}`) ||
128 | (node.data.attributes["data-cy"] && currentTestId === `data-cy = ${node.data.attributes["data-cy"].value}`)
129 | )
130 | )
131 | }
132 | }));
133 | setNodes(temp);
134 | }, [currentHTML, currentTestId]);
135 |
136 | return (
137 |
138 |
145 |
146 |
150 | COMP
151 |
152 |
153 |
154 |
155 | );
156 | };
157 |
158 | export default HtmlFlow;
159 |
--------------------------------------------------------------------------------
/my-app/src/components/FormWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | type FormWrapperProps = {
4 | title: string;
5 | children: ReactNode;
6 | };
7 |
8 | export function FormWrapper({ title, children }: FormWrapperProps) {
9 | return (
10 | <>
11 | {title}
12 | {children}
13 | >
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/my-app/src/components/GetFile.tsx:
--------------------------------------------------------------------------------
1 | import { Parser } from "../parser";
2 | import React from "react";
3 | import { Tree } from "../types/Tree";
4 |
5 | type GetFileProps = {
6 | setter: (tre: Tree) => void;
7 | };
8 |
9 | const GetFile = ({ setter }: GetFileProps) => {
10 | const [fileName, setFileName] = React.useState("");
11 |
12 | function parseTree() {
13 | const file = (document.getElementById("theInputFile") as HTMLInputElement)
14 | .files[0];
15 | const DaParser = new Parser(file.path);
16 | DaParser.parse();
17 | setFileName(DaParser.tree.fileName);
18 | setter(DaParser.tree);
19 | }
20 |
21 | return (
22 |
23 |
24 | Choose Root Component (e.g., App.jsx):
25 |
26 |
33 |
34 |
35 | {fileName.length === 0
36 | ? "No file selected"
37 | : `Currently Selected: ${fileName}`}
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default GetFile;
45 |
--------------------------------------------------------------------------------
/my-app/src/components/ItBlockPage.tsx:
--------------------------------------------------------------------------------
1 | import SmallerPreviewPopup from './SmallerPreviewPopup';
2 | const fs = window.require('fs');
3 | const os = window.require('os');
4 | const path = window.require('path')
5 | import React from 'react';
6 |
7 | type ItBlockPageProps = {
8 | setCurrentPageNum: React.Dispatch>
9 | }
10 |
11 | function ItBlockPage({ setCurrentPageNum }: ItBlockPageProps) {
12 | //states
13 | const [code, setCode] = React.useState('');
14 |
15 | //renders the current state of the editor
16 | React.useEffect(() => {
17 | // const filePath = path.join(__dirname, 'src', 'UserTests', 'TestBlock.cy.js');
18 | const filePath = path.join(os.tmpdir(), 'UserTests', 'TestBlock.cy.js');
19 |
20 | const fileContent = fs.readFileSync(filePath, 'utf8')
21 | setCode(fileContent);
22 | }, []);
23 |
24 | //appends the itBlock to the Editor
25 | function createItBlock(): void {
26 | const itText = (document.getElementById('itText') as HTMLInputElement).value
27 | const testFileContent = itBlock(itText);
28 | // const filePath = path.join(__dirname, 'src', 'UserTests', 'TestBlock.cy.js');
29 | const filePath = path.join(os.tmpdir(), 'UserTests', 'TestBlock.cy.js');
30 |
31 | fs.appendFileSync(filePath, testFileContent);
32 | setCurrentPageNum(2);
33 | }
34 |
35 | //creates an it String that will be displayed on the monaco editor
36 | function itBlock(string: string): string {
37 | return '\n\t' + `it('${string}', () => {`;
38 | }
39 |
40 | return (
41 |
42 |
45 |
Name for test:
46 |
51 |
54 | Create it block
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 | export default ItBlockPage;
64 |
--------------------------------------------------------------------------------
/my-app/src/components/PreviewPopup.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MonacoEditor from 'react-monaco-editor';
3 | // const { ipcRenderer } = window.require('electron');
4 | const path = window.require('path');
5 | const fs = window.require('fs');
6 | const os = window.require('os');
7 | type PreviewPopupProps = {
8 | onClose: () => void;
9 | };
10 |
11 | const PreviewPopup: React.FC = ({ onClose }) => {
12 | const [code, setCode] = React.useState('');
13 | const filePreviewPath = path.join(
14 | os.tmpdir(),
15 | 'UserTests',
16 | 'UserTestFile.cy.js',
17 | );
18 | React.useEffect(() => {
19 | setCode(fs.readFileSync(filePreviewPath, 'utf-8'));
20 | }, []);
21 |
22 | const handleEditorChange = (newValue: string) => {
23 | setCode(newValue);
24 | };
25 |
26 | const handleClose = () => {
27 | fs.writeFileSync(filePreviewPath, code);
28 | onClose();
29 | };
30 |
31 | return (
32 |
40 |
41 |
53 |
56 | Close
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default PreviewPopup;
64 |
--------------------------------------------------------------------------------
/my-app/src/components/SelectAction.tsx:
--------------------------------------------------------------------------------
1 | import { FormWrapper } from './FormWrapper';
2 |
3 | type UserData = {
4 | actionType: string;
5 | data1: string;
6 | };
7 |
8 | type UserFormProps = UserData & {
9 | updateFields: (fields: Partial) => void;
10 | };
11 |
12 | function SelectAction({ actionType, data1, updateFields }: UserFormProps) {
13 | return (
14 |
15 | Select Action
16 | updateFields({ actionType: e.target.value })}>
21 | click
22 | hover
23 | 3
24 |
25 | updateFields({ data1: e.target.value })}>
30 |
31 | );
32 | }
33 | export default SelectAction;
34 |
--------------------------------------------------------------------------------
/my-app/src/components/SmallerPreviewPopup.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MonacoEditor from 'react-monaco-editor';
3 | const path = window.require('path')
4 | const fs = window.require('fs');
5 | const os = window.require('os');
6 |
7 |
8 | type SmallerPreviewPopupProps = {
9 | code: string,
10 | setCode: React.Dispatch>;
11 | }
12 |
13 | const SmallerPreviewPopup: React.FC = ({ code, setCode }) => {
14 |
15 | const handleEditorChange = (newValue: string) => {
16 | setCode(newValue);
17 | const filePath = path.join(os.tmpdir(), 'UserTests', 'TestBlock.cy.js');
18 | fs.writeFileSync(filePath, newValue);
19 | };
20 |
21 |
22 |
23 | return (
24 |
25 |
38 |
39 | );
40 | };
41 |
42 | export default SmallerPreviewPopup
43 |
44 |
--------------------------------------------------------------------------------
/my-app/src/components/StatementPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import SmallerPreviewPopup from "./SmallerPreviewPopup";
3 | import DropdownButton from "./DropdownButton";
4 | const fs = window.require("fs");
5 | const os = window.require('os');
6 |
7 | const path = window.require("path");
8 | import { Tree } from "../types/Tree";
9 | import actionOptions from "../options/actionOptions";
10 | import assertionOptions from "../options/assertionOptions";
11 | import otherCommandOptions from "../options/otherCommandOptions";
12 | import { encodingArray } from "../options/optionVariables";
13 |
14 | type ModalCreateCodeType = (string | number)[];
15 |
16 | type StatementPageProps = {
17 | setCurrentPageNum: React.Dispatch>;
18 | currentComponent: Tree;
19 | currentHTML: string;
20 | currentTestId: string;
21 | };
22 |
23 | const StatementPage: React.FC = ({
24 | setCurrentPageNum,
25 | currentComponent,
26 | currentHTML,
27 | currentTestId,
28 | }) => {
29 | //state variables
30 | const [selectedOptions, setSelectedOptions] = useState([]);
31 | const [dataCy, setDataCy] = useState("");
32 | const [code, setCode] = React.useState("");
33 | const [empty, setEmpty] = React.useState("");
34 | // const filePath = path.join(__dirname, 'src', "UserTests", "TestBlock.cy.js");
35 | const filePath = path.join(os.tmpdir(), 'UserTests', 'TestBlock.cy.js');
36 |
37 | // const filePreviewPath = path.join(
38 | // __dirname,
39 | // 'src',
40 | // "UserTests",
41 | // "UserTestFile.cy.js"
42 | // );
43 |
44 | const filePreviewPath = path.join(
45 | os.tmpdir(),
46 | "UserTests",
47 | "UserTestFile.cy.js"
48 | );
49 |
50 | const [dropDown, setDropDown] = useState("");
51 | //renders current state of testblock.cy.js onto the monaco editor
52 | React.useEffect(() => {
53 | // const filePath = path.join(__dirname, 'src', 'UserTests', 'TestBlock.cy.js');
54 | const filePath = path.join(os.tmpdir(), 'UserTests', 'TestBlock.cy.js');
55 | const fileContent = fs.readFileSync(filePath, "utf8");
56 | setCode(fileContent);
57 | }, []);
58 |
59 | //whenever we grab our data-cy test Id from the prop, we set it in state and is used as the testId for the component tests
60 | React.useEffect(() => {
61 | setDataCy(currentTestId);
62 | }, [currentTestId]);
63 |
64 | //function is invoked whenever a user selects one of the option in the dropdown and reassigns state so that it appears in the statement bar
65 | const handleOptionClick = (option: string) => {
66 | setSelectedOptions([...selectedOptions, option]);
67 | };
68 |
69 | //a function attached to a button to append the Itblock onto the editor
70 | function endItBlock() {
71 | fs.appendFileSync(filePath, "})");
72 | setCurrentPageNum(1);
73 | }
74 |
75 | function endDescribeBlock() {
76 | fs.appendFileSync(filePath, "\n\t" + "})" + "\n" + "})");
77 | setCurrentPageNum(0);
78 | const testBlockContent = fs.readFileSync(filePath, "utf8");
79 | fs.writeFileSync(filePreviewPath, testBlockContent);
80 | }
81 |
82 | function endStatement() {
83 | fs.appendFileSync(filePath, "\n\t\t" + selectedOptions.join(""));
84 | setSelectedOptions([]);
85 | const fileContent = fs.readFileSync(filePath, "utf8");
86 | setCode(fileContent);
87 | }
88 |
89 | const queryOptions = {
90 | as: {
91 | option: "as",
92 | code: `.as()`,
93 | tooltip: "Retrieve and alias elements.",
94 | modal: [
95 | { type: "label", labelText: "Retrieve and alias elements." },
96 | {
97 | type: "input",
98 | inputType:
99 | "The name of the alias to be referenced later within a cy.get() or cy.wait() command using an @ prefix.",
100 | },
101 | ],
102 | modalCreateCode: function (args: ModalCreateCodeType): string {
103 | if (args[0] !== empty && args[1] === empty) {
104 | return `.as('${args[0]}')`;
105 | } else if (args[0] !== empty && args[1] !== empty) {
106 | return `.as('${args[0]}', '${args[1]}')`;
107 | } else {
108 | return;
109 | }
110 | },
111 | },
112 | children: {
113 | option: "Children",
114 | code: `.children()`,
115 | tooltip: "Select child elements.",
116 | },
117 | closest: {
118 | option: "Closest",
119 | code: `.closest()`,
120 | tooltip: "Find nearest matching ancestor.",
121 | modal: [
122 | { type: "label", labelText: "Find nearest matching ancestor." },
123 | {
124 | type: "input",
125 | inputType: "A selector used to filter matching DOM elements.",
126 | },
127 | ],
128 | modalCreateCode: function (text: ModalCreateCodeType): string {
129 | if (text[0]) {
130 | return `.closest('${text[0]}')`;
131 | } else {
132 | return;
133 | }
134 | },
135 | },
136 | contains: {
137 | option: "Contains",
138 | code: `.contains('[${dataCy}]')`,
139 | tooltip: "Locate element with specified text (Chained off Dom El).",
140 | modal: [
141 | { type: "label", labelText: "Locate element with specified text." },
142 | {
143 | type: "input",
144 | inputType: "Get the DOM element containing the content.",
145 | },
146 | {
147 | type: "input",
148 | inputType:
149 | "Specify a selector to filter DOM elements containing the text. Cypress will ignore its default preference order for the specified selector. Using a selector allows you to return more shallow elements (higher in the tree) that contain the specific text.",
150 | },
151 | ],
152 | modalCreateCode: function (text: ModalCreateCodeType): string {
153 | if (text[0] !== empty && text[1] === empty) {
154 | const value = typeof text[0] === 'number' ? `'${text[0]}'` : text[0];
155 | return `.contains(${value})`;
156 | } else if (text[0] !== empty && text[1] !== empty) {
157 | const value = typeof text[1] === 'number' ? `'${text[1]}'` : text[1];
158 | return `.contains('${text[0]}', ${value})`;
159 | } else {
160 | return;
161 | }
162 | },
163 | },
164 | containsCy: {
165 | option: "Cy.Contains",
166 | code: `cy.contains('[${dataCy}]')`,
167 | tooltip: "Locate element with specified text (Chained off Cy).",
168 | modal: [
169 | { type: "label", labelText: "Locate element with specified text." },
170 | {
171 | type: "input",
172 | inputType: "Get the DOM element containing the content.",
173 | },
174 | {
175 | type: "input",
176 | inputType:
177 | "Specify a selector to filter DOM elements containing the text. Cypress will ignore its default preference order for the specified selector. Using a selector allows you to return more shallow elements (higher in the tree) that contain the specific text.",
178 | },
179 | ],
180 | modalCreateCode: function (text: ModalCreateCodeType): string {
181 | if (text[0] !== empty && text[1] === empty) {
182 | const value = typeof text[0] === 'number' ? `'${text[0]}'` : text[0];
183 | return `cy.contains(${value})`;
184 | } else if (text[0] !== empty && text[1] !== empty) {
185 | const value = typeof text[1] === 'number' ? `'${text[1]}'` : text[1];
186 | return `cy.contains('${text[0]}', ${value})`;
187 | } else {
188 | return;
189 | }
190 | },
191 | },
192 | document: {
193 | option: "Document",
194 | code: `cy.document()`,
195 | tooltip: "Access the document object.",
196 | },
197 | eq: {
198 | option: "Eq",
199 | code: `.eq()`,
200 | tooltip: "Select by index position.",
201 | modal: [
202 | { type: "label", labelText: "Select by index position." },
203 | {
204 | type: "input",
205 | inputType:
206 | "A number indicating the index to find the element at within an array of elements. A negative number indicates the index position from the end to find the element at within an array of elements.",
207 | },
208 | ],
209 | modalCreateCode: function (text: ModalCreateCodeType): string {
210 | if (text[0]) {
211 | return `.eq('${text[0]}')`;
212 | } else {
213 | return;
214 | }
215 | },
216 | },
217 | filter: {
218 | option: "Filter",
219 | code: `.filter()`,
220 | tooltip: "Filter elements by selector.",
221 | modal: [
222 | { type: "label", labelText: "Filter elements by selector." },
223 | {
224 | type: "input",
225 | inputType: "A selector used to filter matching DOM elements.",
226 | },
227 | ],
228 | modalCreateCode: function (text: ModalCreateCodeType): string {
229 | if (text[0]) {
230 | return `.filter('${text[0]}')`;
231 | } else {
232 | return;
233 | }
234 | },
235 | },
236 | find: {
237 | option: "Find",
238 | code: `.find()`,
239 | tooltip: "Search for nested elements.",
240 | modal: [
241 | { type: "label", labelText: "Search for nested elements." },
242 | {
243 | type: "input",
244 | inputType: "A selector used to filter matching DOM elements.",
245 | },
246 | ],
247 | modalCreateCode: function (text: ModalCreateCodeType): string {
248 | if (text[0]) {
249 | return `.find('${text[0]}')`;
250 | } else {
251 | return;
252 | }
253 | },
254 | },
255 | first: {
256 | option: "First",
257 | code: `.first()`,
258 | tooltip: "Select the first element.",
259 | },
260 | focused: {
261 | option: "Focused",
262 | code: `cy.focused()`,
263 | tooltip: "Select the focused element.",
264 | },
265 | get: {
266 | option: "Get",
267 | code: `cy.get('[${dataCy}]')`,
268 | tooltip: "Select elements by selector.",
269 | },
270 | hash: {
271 | option: "Hash",
272 | code: `cy.hash()`,
273 | tooltip: "Access the URL hash.",
274 | },
275 | its: {
276 | option: "Its",
277 | code: `.its()`,
278 | tooltip: "Access element properties.",
279 | modal: [
280 | { type: "label", labelText: "Access element properties." },
281 | {
282 | type: "input",
283 | inputType:
284 | "Index, name of property or name of nested properties (with dot notation) to get.",
285 | },
286 | ],
287 | modalCreateCode: function (text: ModalCreateCodeType): string {
288 | if (text[0]) {
289 | const value = typeof text[0] === 'number' ? `'${text[0]}'` : text[0];
290 | return `.its(${value})`;
291 | } else {
292 | return;
293 | }
294 | },
295 | },
296 | last: {
297 | option: "Last",
298 | code: `.last()`,
299 | tooltip: "Select the last element.",
300 | },
301 | location: {
302 | option: "Location",
303 | code: `cy.location()`,
304 | tooltip: "Access the URL location.",
305 | modal: [
306 | { type: "label", labelText: "Access the URL location." },
307 | {
308 | type: "input",
309 | inputType:
310 | "OPTIONAL: A key on the location object. Returns this value instead of the full location object.",
311 | },
312 | ],
313 | modalCreateCode: function (text: ModalCreateCodeType): string {
314 | if (text[0] === empty) {
315 | return `cy.location()`;
316 | } else {
317 | return `cy.location('${text[0]}')`;
318 | }
319 | },
320 | },
321 | next: {
322 | option: "Next",
323 | code: `.next()`,
324 | tooltip: "Select the next sibling element.",
325 | modal: [
326 | { type: "label", labelText: "Select the next sibling element." },
327 | {
328 | type: "input",
329 | inputType:
330 | "OPTIONAL: A selector used to filter matching DOM elements.",
331 | },
332 | ],
333 | modalCreateCode: function (text: ModalCreateCodeType): string {
334 | if (text[0] === empty) {
335 | return `.next()`;
336 | } else {
337 | return `.next('${text[0]}')`;
338 | }
339 | },
340 | },
341 | nextAll: {
342 | option: "NextAll",
343 | code: `.nextAll()`,
344 | tooltip: "Select all next siblings.",
345 | modal: [
346 | { type: "label", labelText: "Select all next siblings." },
347 | {
348 | type: "input",
349 | inputType:
350 | "OPTIONAL: A selector used to filter matching DOM elements.",
351 | },
352 | ],
353 | modalCreateCode: function (text: ModalCreateCodeType): string {
354 | if (text[0] === empty) {
355 | return `.nextAll()`;
356 | } else {
357 | return `.nextAll('${text[0]}')`;
358 | }
359 | },
360 | },
361 | nextUntil: {
362 | option: "NextUntil",
363 | code: `.nextUntil()`,
364 | tooltip: "Select until specified sibling.",
365 | modal: [
366 | { type: "label", labelText: "Select until specified sibling." },
367 | {
368 | type: "input",
369 | inputType:
370 | "The selector where you want finding next siblings to stop.",
371 | },
372 | {
373 | type: "input",
374 | inputType:
375 | "OPTIONAL: A selector used to filter matching DOM elements.",
376 | },
377 | ],
378 | modalCreateCode: function (args: ModalCreateCodeType): string {
379 | if (args[0] !== empty && args[1] === empty) {
380 | return `.nextUntil('${args[0]}')`;
381 | } else if (args[0] !== empty && args[1] !== empty) {
382 | return `.nextUntil('${args[0]}', '${args[1]}')`;
383 | } else {
384 | return;
385 | }
386 | },
387 | },
388 | not: {
389 | option: "Not",
390 | code: `.not()`,
391 | tooltip: "Exclude elements by selector.",
392 | modal: [
393 | { type: "label", labelText: "Exclude elements by selector." },
394 | {
395 | type: "input",
396 | inputType: "A selector used to remove matching DOM elements.",
397 | },
398 | ],
399 | modalCreateCode: function (text: ModalCreateCodeType): string {
400 | if (text[0]) {
401 | return `.not('${text[0]}')`;
402 | } else {
403 | return;
404 | }
405 | },
406 | },
407 | parent: {
408 | option: "Parent",
409 | code: `.parent()`,
410 | tooltip: "Select the parent element.",
411 | modal: [
412 | { type: "label", labelText: "Select the parent element." },
413 | {
414 | type: "input",
415 | inputType:
416 | "OPTIONAL: A selector used to filter matching DOM elements.",
417 | },
418 | ],
419 | modalCreateCode: function (text: ModalCreateCodeType): string {
420 | if (text[0] === empty) {
421 | return `.parent()`;
422 | } else {
423 | return `.parent('${text[0]}')`;
424 | }
425 | },
426 | },
427 | parents: {
428 | option: "Parents",
429 | code: `.parents()`,
430 | tooltip: "Select all ancestor elements.",
431 | modal: [
432 | { type: "label", labelText: "Select all ancestor elements." },
433 | {
434 | type: "input",
435 | inputType:
436 | "OPTIONAL: A selector used to filter matching DOM elements.",
437 | },
438 | ],
439 | modalCreateCode: function (text: ModalCreateCodeType): string {
440 | if (text[0] === empty) {
441 | return `.parents()`;
442 | } else {
443 | return `.parents('${text[0]}')`;
444 | }
445 | },
446 | },
447 | parentsUntil: {
448 | option: "ParentsUntil",
449 | code: `.parentsUntil()`,
450 | tooltip: "Select ancestors until specified.",
451 | modal: [
452 | { type: "label", labelText: "Select ancestors until specified." },
453 | {
454 | type: "input",
455 | inputType:
456 | "The selector where you want finding parent ancestors to stop.",
457 | },
458 | {
459 | type: "input",
460 | inputType:
461 | "OPTIONAL: A selector used to filter matching DOM elements.",
462 | },
463 | ],
464 | modalCreateCode: function (args: ModalCreateCodeType): string {
465 | if (args[0] !== empty && args[1] === empty) {
466 | return `.parentsUntil('${args[0]}')`;
467 | } else if (args[0] !== empty && args[1] !== empty) {
468 | return `.parentsUntil('${args[0]}', '${args[1]}')`;
469 | } else {
470 | return;
471 | }
472 | },
473 | },
474 | prev: {
475 | option: "Prev",
476 | code: `.prev()`,
477 | tooltip: "Select the previous sibling element.",
478 | modal: [
479 | { type: "label", labelText: "Select the previous sibling element." },
480 | {
481 | type: "input",
482 | inputType:
483 | "OPTIONAL: A selector used to filter matching DOM elements.",
484 | },
485 | ],
486 | modalCreateCode: function (text: ModalCreateCodeType): string {
487 | if (text[0] === empty) {
488 | return `.prev()`;
489 | } else {
490 | return `.prev('${text[0]}')`;
491 | }
492 | },
493 | },
494 | prevAll: {
495 | option: "PrevAll",
496 | code: `.prevAll()`,
497 | tooltip: "Select all previous siblings.",
498 | modal: [
499 | { type: "label", labelText: "Select all previous siblings." },
500 | {
501 | type: "input",
502 | inputType:
503 | "OPTIONAL: A selector used to filter matching DOM elements.",
504 | },
505 | ],
506 | modalCreateCode: function (text: ModalCreateCodeType): string {
507 | if (text[0] === empty) {
508 | return `.prevAll()`;
509 | } else {
510 | return `.prevAll('${text[0]}')`;
511 | }
512 | },
513 | },
514 | prevUntil: {
515 | option: "PrevUntil",
516 | code: `.prevUntil()`,
517 | tooltip: "Select until specified sibling.",
518 | modal: [
519 | { type: "label", labelText: "Select until specified sibling." },
520 | {
521 | type: "input",
522 | inputType:
523 | "The selector where you want finding previous siblings to stop.",
524 | },
525 | {
526 | type: "input",
527 | inputType:
528 | "OPTIONAL: A selector used to filter matching DOM elements.",
529 | },
530 | ],
531 | modalCreateCode: function (args: ModalCreateCodeType): string {
532 | if (args[0] !== empty && args[1] === empty) {
533 | return `.prevUntil('${args[0]}')`;
534 | } else if (args[0] !== empty && args[1] !== empty) {
535 | return `.prevUntil('${args[0]}', '${args[1]}')`;
536 | } else {
537 | return;
538 | }
539 | },
540 | },
541 | readFile: {
542 | option: "Readfile",
543 | code: `cy.readFile()`,
544 | tooltip: "Read and parse a file.",
545 | modal: [
546 | { type: "label", labelText: "Read and parse a file." },
547 | {
548 | type: "input",
549 | inputType: "A path to a file within the project root ",
550 | },
551 | { type: "select", options: encodingArray },
552 | ],
553 | modalCreateCode: function (args: ModalCreateCodeType): string {
554 | if (args[0] !== empty && args[1] === empty) {
555 | return `cy.readFile('${args[0]}')`;
556 | } else if (args[0] !== empty && args[1] !== empty) {
557 | return `cy.readFile('${args[0]}', '${args[1]}')`;
558 | } else {
559 | return;
560 | }
561 | },
562 | },
563 | Root: {
564 | option: "Root",
565 | code: `cy.root()`,
566 | tooltip: "Select the root element.",
567 | },
568 | shadow: {
569 | option: "Shadow",
570 | code: `.shadow()`,
571 | tooltip: "Select shadow DOM elements.",
572 | },
573 | siblings: {
574 | option: "Siblings",
575 | code: `.siblings()`,
576 | tooltip: "Select all siblings.",
577 | modal: [
578 | { type: "label", labelText: "Select all siblings." },
579 | {
580 | type: "input",
581 | inputType:
582 | "OPTIONAL: A selector used to filter matching DOM elements.",
583 | },
584 | ],
585 | modalCreateCode: function (text: ModalCreateCodeType): string {
586 | if (text[0] === empty) {
587 | return `.siblings()`;
588 | } else {
589 | return `.siblings('${text[0]}')`;
590 | }
591 | },
592 | },
593 | title: {
594 | option: "Title",
595 | code: `cy.title()`,
596 | tooltip: "Access the document title.",
597 | },
598 | url: {
599 | option: "URL",
600 | code: `cy.url()`,
601 | tooltip: "Access the current URL.",
602 | },
603 | window: {
604 | option: "Window",
605 | code: `cy.window()`,
606 | tooltip: "Access the window object.",
607 | },
608 | };
609 |
610 | return (
611 |
612 |
613 | {/* Button and Dropdowns */}
614 |
615 |
622 |
629 |
636 |
643 |
644 |
645 | {/* Statement Bar */}
646 |
647 |
648 | {selectedOptions.join("")}
649 |
650 |
{
652 | setSelectedOptions(selectedOptions.slice(0, -1));
653 | }}
654 | className="border-2 border-secondary hover:bg-primaryDark p-2"
655 | >BACK
656 |
657 |
658 |
659 |
660 | {/* Currently Selected Bar */}
661 |
662 |
663 | Currently selected:
664 | {currentHTML}
665 | {currentComponent &&
666 | currentComponent.name &&
667 | ` in ${currentComponent.name}`}
668 | {currentTestId && ` with ${currentTestId}`}
669 |
670 |
671 | {/* End Block Buttons */}
672 |
673 |
677 | End describe block
678 |
679 |
683 | End it block
684 |
685 |
689 | End statement
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 | );
699 | };
700 |
701 | export default StatementPage;
702 |
--------------------------------------------------------------------------------
/my-app/src/components/TestGenContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement, useState } from 'react';
2 | import DescribePage from './DescribePage';
3 | import ItBlockPage from './ItBlockPage';
4 | import StatementPage from './StatementPage';
5 | import { Tree } from './../types/Tree'
6 |
7 | type TestGenContainerProps = {
8 | currentComponent: Tree,
9 | currentHTML: string,
10 | currentTestId: string,
11 | }
12 |
13 | function TestGenContainer({currentComponent, currentHTML, currentTestId}: TestGenContainerProps) {
14 | const [currentPageNum, setCurrentPageNum] = useState(0);
15 | const arrayOfReact: ReactElement[] = [
16 | ,
17 | ,
18 | ,
19 | ];
20 | return (
21 |
22 | {arrayOfReact[currentPageNum]}
23 |
24 | );
25 | }
26 | export default TestGenContainer;
27 |
--------------------------------------------------------------------------------
/my-app/src/components/TestingUi.tsx:
--------------------------------------------------------------------------------
1 | // import React, { useState, FormEvent } from 'react';
2 | // import { useMultistepForm } from './useMultiStepForm';
3 | // import SelectAction from './SelectAction';
4 |
5 | // type FormData = {
6 | // actionType: string;
7 | // selectedItem: string;
8 | // data1: string;
9 | // data2: string;
10 | // data3: string;
11 | // };
12 |
13 | // const INITIAL_DATA: FormData = {
14 | // actionType: '',
15 | // selectedItem: '',
16 | // data1: '',
17 | // data2: '',
18 | // data3: '',
19 | // };
20 |
21 | // const TestingUi = () => {
22 | // const [data, setData] = useState(INITIAL_DATA);
23 | // function updateFields(fields: Partial) {
24 | // setData(prev => {
25 | // return { ...prev, ...fields };
26 | // });
27 | // }
28 |
29 | // const { steps, currentStepIndex, step, isFirstStep, isLastStep, back, next } =
30 | // useMultistepForm([
31 | // ,
32 | //
,
33 | // ,
34 | // ]);
35 | // function onSubmit(e: FormEvent) {
36 | // e.preventDefault();
37 | // if (!isLastStep) return next();
38 | // alert('we gonna add an action');
39 | // }
40 | // return (
41 | //
42 | //
console.log('clicked')}>
43 | // create new assertation
44 | //
45 | //
console.log('clicked')}>
48 | // export test
49 | //
50 | //
51 | //
65 | //
66 | // );
67 | // };
68 |
69 | // export default TestingUi;
70 |
--------------------------------------------------------------------------------
/my-app/src/components/Webview.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Tree } from "../types/Tree";
3 |
4 | type WebViewProps = {
5 | url: string;
6 | currentComponent: Tree;
7 | currentTestId: string;
8 | setData: React.Dispatch>;
9 | };
10 |
11 | const Webview = (props: WebViewProps) => {
12 | const { url, currentComponent, setData, currentTestId } = props;
13 | const webviewRef = React.useRef(null);
14 | const [isWebviewReady, setIsWebviewReady] = React.useState(false);
15 | const [oldComponent, setOldComponent] = React.useState([null, null]);
16 | const [oldHTML, setOldHTML] = React.useState([null, null, null]);
17 |
18 | // Set up the dom-ready event listener
19 | React.useEffect(() => {
20 | const handleDomReady = () => setIsWebviewReady(true);
21 | const webview = webviewRef.current;
22 | webview?.addEventListener("dom-ready", handleDomReady);
23 |
24 | return () => {
25 | webview?.removeEventListener("dom-ready", handleDomReady);
26 | };
27 | }, []);
28 |
29 | React.useEffect(() => {
30 | if (currentComponent && isWebviewReady) {
31 | const webview = document.getElementById(
32 | "webview"
33 | ) as Electron.WebviewTag | null;
34 | webview
35 | .executeJavaScript(
36 | // Use a try-catch block to handle any potential errors
37 | `try {
38 | // Use the provided CSS selector to select the element
39 | let selectedComponent = document.querySelector('${currentComponent.htmlChildrenTestIds[0]}');
40 | let oldComponent = document.querySelector('${oldComponent[1]}');
41 | let oldBorder = selectedComponent ? window.getComputedStyle(selectedComponent).border : 'none';
42 | let result = null;
43 | // Check if the element exists
44 | if (selectedComponent) {
45 | selectedComponent.style.border = "2px solid #1DF28F"
46 | // Get the outerHTML of the element
47 | result = selectedComponent.outerHTML;
48 | }
49 | if (oldComponent){
50 | oldComponent.style.border = "${oldComponent[0]}"
51 | }
52 |
53 |
54 | // Return the result
55 | [result, oldBorder, '${currentComponent.htmlChildrenTestIds[0]}'];
56 | } catch (error) {
57 | // Handle any errors that occur during execution
58 | error.message; // You can return the error message for debugging
59 | }`
60 | )
61 | .then((resultFromWebview: [string, string, string]) => {
62 | // Here, use the result from the webview to update your React component state
63 | const [result, oldBorder, testid] = resultFromWebview;
64 | if (result) {
65 | setOldComponent([oldBorder, testid]);
66 | setData(result);
67 | }
68 | });
69 | }
70 | }, [currentComponent, isWebviewReady]);
71 |
72 |
73 | React.useEffect(() => {
74 | if (currentTestId) {
75 | const webview = document.getElementById(
76 | "webview"
77 | ) as Electron.WebviewTag | null;
78 | webview
79 | .executeJavaScript(
80 | `
81 | try{
82 | let selectedHTML = document.querySelector('[${currentTestId}]');
83 | let oldHTML = document.querySelector('[${oldHTML[1]}]');
84 | let oldBorder = selectedHTML ? window.getComputedStyle(selectedHTML).border : 'none';
85 | let flashing = false;
86 | let intervalId;
87 | if (selectedHTML) {
88 | intervalId = setInterval(() =>{
89 | selectedHTML.style.border = flashing ? oldBorder : "3px solid #048C7F";
90 | flashing = !flashing
91 | }, 1000)
92 |
93 | };
94 | if (oldHTML){
95 | clearInterval(${oldHTML[2]})
96 | oldHTML.style.border = "${oldHTML[0]}"
97 | }
98 | [oldBorder, '${currentTestId}', intervalId]
99 | } catch (error) {
100 | // Handle any errors that occur during execution
101 | error.message; // You can return the error message for debugging
102 | }
103 | `
104 | )
105 | .then((resultFromWebview: any[]) => {
106 | // Here, use the result from the webview to update your React component state
107 | const [oldBorder, oldTestid, intervalId] = resultFromWebview;
108 | if (oldBorder) {
109 | setOldHTML([oldBorder, oldTestid, intervalId ]);
110 | }
111 | });
112 | }
113 | }, [currentTestId]);
114 |
115 | return (
116 |
117 |
123 |
124 | );
125 | };
126 | export default Webview;
127 |
--------------------------------------------------------------------------------
/my-app/src/components/useMultiStepForm.ts:
--------------------------------------------------------------------------------
1 | // import { ReactElement, useState } from 'react';
2 |
3 | // export function useMultistepForm(steps: ReactElement[]) {
4 | // const [currentStepIndex, setCurrentStepIndex] = useState(0);
5 |
6 | // function next() {
7 | // setCurrentStepIndex(i => {
8 | // if (i >= steps.length - 1) return i;
9 | // return i + 1;
10 | // });
11 | // }
12 |
13 | // function back() {
14 | // setCurrentStepIndex(i => {
15 | // if (i <= 0) return i;
16 | // return i - 1;
17 | // });
18 | // }
19 |
20 | // function goTo(index: number) {
21 | // setCurrentStepIndex(index);
22 | // }
23 |
24 | // return {
25 | // currentStepIndex,
26 | // step: steps[currentStepIndex],
27 | // steps,
28 | // isFirstStep: currentStepIndex === 0,
29 | // isLastStep: currentStepIndex === steps.length - 1,
30 | // goTo,
31 | // next,
32 | // back,
33 | // };
34 | // }
35 |
--------------------------------------------------------------------------------
/my-app/src/emptyFile.cy.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Cydekick/efd8edf339389abdc3687c350bf4aa6b704fa6a7/my-app/src/emptyFile.cy.js
--------------------------------------------------------------------------------
/my-app/src/getNonce.ts:
--------------------------------------------------------------------------------
1 | export function getNonce(): string {
2 | let text = '';
3 | const possible =
4 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
5 | for (let i = 0; i < 32; i++) {
6 | text += possible.charAt(Math.floor(Math.random() * possible.length));
7 | }
8 | return text;
9 | }
10 |
--------------------------------------------------------------------------------
/my-app/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body {
7 | height: 100%;
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/my-app/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/my-app/src/index.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, ipcMain, dialog } from 'electron';
2 | // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
3 | // plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
4 | // whether you're running in development or production).
5 | import fs from 'fs';
6 | import * as os from 'os';
7 | import * as path from 'path';
8 | // import { initialize, enable } from '@electron/remote/main';
9 | declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
10 | declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
11 |
12 | // initialize();
13 |
14 | // Handle creating/removing shortcuts on Windows when installing/uninstalling.
15 | if (require('electron-squirrel-startup')) {
16 | app.quit();
17 | }
18 |
19 | const createWindow = (): void => {
20 | // Create the browser window.
21 | const mainWindow = new BrowserWindow({
22 | height: 800,
23 | width: 1200,
24 | webPreferences: {
25 | preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
26 | nodeIntegration: true,
27 | contextIsolation: false,
28 | webviewTag: true,
29 | },
30 | icon: '../logo.png'
31 | });
32 |
33 | // enable(mainWindow.webContents);
34 |
35 | // and load the index.html of the app.
36 | mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
37 |
38 | // Open the DevTools.
39 | mainWindow.webContents.openDevTools();
40 | };
41 |
42 | // This method will be called when Electron has finished
43 | // initialization and is ready to create browser windows.
44 | // Some APIs can only be used after this event occurs.
45 | // app.on('ready', createWindow);
46 | app.on('ready', () => {
47 | const tempDir = path.join(os.tmpdir(), 'UserTests');
48 | fs.mkdirSync(tempDir, { recursive: true });
49 |
50 | const files = ['UserTestFile.cy.js', 'TestBlock.cy.js'];
51 | files.forEach(file => {
52 | const filePath = path.join(tempDir, file);
53 | fs.writeFileSync(filePath, ''); // creates file with empty content
54 | });
55 |
56 | createWindow();
57 | });
58 |
59 |
60 | // Quit when all windows are closed, except on macOS. There, it's common
61 | // for applications and their menu bar to stay active until the user quits
62 | // explicitly with Cmd + Q.
63 | app.on('window-all-closed', () => {
64 | if (process.platform !== 'darwin') {
65 | app.quit();
66 | }
67 | });
68 |
69 | app.on('activate', () => {
70 | // On OS X it's common to re-create a window in the app when the
71 | // dock icon is clicked and there are no other windows open.
72 | if (BrowserWindow.getAllWindows().length === 0) {
73 | createWindow();
74 | }
75 | });
76 |
77 | ipcMain.handle('save-file', async (event, content) => {
78 | const { filePath } = await dialog.showSaveDialog({
79 | title: 'Save your file',
80 | defaultPath: 'UserTestFile.cy.js',
81 | filters: [{ name: 'JavaScript', extensions: ['js'] }],
82 | });
83 |
84 | if (filePath) {
85 | fs.writeFileSync(filePath, content);
86 | }
87 | });
--------------------------------------------------------------------------------
/my-app/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import App from './components/App';
3 |
4 | const root = createRoot(document.getElementById('root'));
5 |
6 | root.render(
7 |
8 | );
9 |
10 |
11 |
--------------------------------------------------------------------------------
/my-app/src/options/actionOptions.ts:
--------------------------------------------------------------------------------
1 | import { empty } from "./optionVariables";
2 |
3 | type ModalCreateCodeType = (string | number)[];
4 |
5 | const actionOptions = {
6 | check: {
7 | option: "Check",
8 | code: ".check()",
9 | tooltip: "Check checkbox(es) or radio(s).",
10 | modal: [
11 | { type: "label", labelText: "Check checkbox(es) or radio(s)." },
12 | {
13 | type: "input",
14 | inputType:
15 | "OPTIONAL: Value of checkbox or radio that should be checked.",
16 | },
17 | ],
18 | modalCreateCode: function (args: ModalCreateCodeType): string {
19 | if (args[0] === empty) {
20 | return `.check()`;
21 | } else {
22 | return `.check('${args[0]}')`;
23 | }
24 | },
25 | },
26 | clear: {
27 | option: "Clear",
28 | code: ".clear()",
29 | tooltip: "Clear the input field.",
30 | },
31 | click: {
32 | option: "Click",
33 | code: ".click()",
34 | tooltip: "Click the element.",
35 | modal: [
36 | { type: "label", labelText: "Click the element." },
37 | {
38 | type: "input",
39 | inputType:
40 | "OPTIONAL: A specified position to scroll the window or element to. Valid positions are topLeft, top, topRight, left, center, right, bottomLeft, bottom, and bottomRight.",
41 | },
42 | {
43 | type: "input",
44 | inputType:
45 | "OPTIONAL: (x,y) x: The distance in pixels from window/element's left or percentage of the window/element's width to scroll to. y: the distance in pixels from window/element's top or percentage of the window/element's height to scroll to.",
46 | },
47 | ],
48 | modalCreateCode: function (args: ModalCreateCodeType): string {
49 | if (args[0] === empty && args[1] === empty) {
50 | return `.click()`;
51 | } else if (args[1] === empty) {
52 | return `.click('${args[0]}')`;
53 | } else if (args[0] !== empty && args[1] !== empty) {
54 | const value1: string | number = isNaN(Number(args[0]))
55 | ? `'${args[0]}'`
56 | : Number(args[0]);
57 | const value2: string | number = isNaN(Number(args[1]))
58 | ? `'${args[1]}'`
59 | : Number(args[1]);
60 | return `.click(${value1}, ${value2})`;
61 | }
62 | },
63 | },
64 | doubleClick: {
65 | option: "Double Click",
66 | code: ".dblclick()",
67 | tooltip: "Double-click the element.",
68 | modal: [
69 | { type: "label", labelText: "Double-click the element." },
70 | {
71 | type: "input",
72 | inputType:
73 | "OPTIONAL: A specified position to scroll the window or element to. Valid positions are topLeft, top, topRight, left, center, right, bottomLeft, bottom, and bottomRight.",
74 | },
75 | {
76 | type: "input",
77 | inputType:
78 | "OPTIONAL: (x,y) x: The distance in pixels from window/element's left or percentage of the window/element's width to scroll to. y: the distance in pixels from window/element's top or percentage of the window/element's height to scroll to.",
79 | },
80 | ],
81 | modalCreateCode: function (args: ModalCreateCodeType): string {
82 | if (args[0] === empty && args[1] === empty) {
83 | return `.dblclick()`;
84 | } else if (args[1] === empty) {
85 | return `.dblclick('${args[0]}')`;
86 | } else if (args[0] !== empty && args[1] !== empty) {
87 | const value1: string | number = isNaN(Number(args[0]))
88 | ? `'${args[0]}'`
89 | : Number(args[0]);
90 | const value2: string | number = isNaN(Number(args[1]))
91 | ? `'${args[1]}'`
92 | : Number(args[1]);
93 | return `.dblclick(${value1}, ${value2})`;
94 | }
95 | },
96 | },
97 | rightClick: {
98 | option: "Right Click",
99 | code: ".rightclick()",
100 | tooltip: "Right-click the element.",
101 | modal: [
102 | { type: "label", labelText: "Right-click the element." },
103 | {
104 | type: "input",
105 | inputType:
106 | "OPTIONAL: A specified position to scroll the window or element to. Valid positions are topLeft, top, topRight, left, center, right, bottomLeft, bottom, and bottomRight.",
107 | },
108 | {
109 | type: "input",
110 | inputType:
111 | "OPTIONAL: (x,y) x: The distance in pixels from window/element's left or percentage of the window/element's width to scroll to. y: the distance in pixels from window/element's top or percentage of the window/element's height to scroll to.",
112 | },
113 | ],
114 | modalCreateCode: function (args: ModalCreateCodeType): string {
115 | if (args[0] === empty && args[1] === empty) {
116 | return `.rightclick()`;
117 | } else if (args[1] === empty) {
118 | return `.rightclick('${args[0]}')`;
119 | } else if (args[0] !== empty && args[1] !== empty) {
120 | const value1: string | number = isNaN(Number(args[0]))
121 | ? `'${args[0]}'`
122 | : Number(args[0]);
123 | const value2: string | number = isNaN(Number(args[1]))
124 | ? `'${args[1]}'`
125 | : Number(args[1]);
126 | return `.rightclick(${value1}, ${value2})`;
127 | }
128 | },
129 | },
130 | scrollIntoView: {
131 | option: "ScrollIntoView",
132 | code: ".scrollIntoView()",
133 | tooltip: "Scroll the element into view.",
134 | },
135 | scrollTo: {
136 | option: "ScrollTo",
137 | code: ".scrollTo()",
138 | tooltip: "Scroll the element to a specific position (Chained Off Dom el).",
139 | modal: [
140 | {
141 | type: "label",
142 | labelText: "Scroll the element to a specific position.",
143 | },
144 | {
145 | type: "input",
146 | inputType:
147 | "A specified position to scroll the window or element to. Valid positions are topLeft, top, topRight, left, center, right, bottomLeft, bottom, and bottomRight.",
148 | },
149 | {
150 | type: "input",
151 | inputType:
152 | "(x,y) x: The distance in pixels from window/element's left or percentage of the window/element's width to scroll to. y: the distance in pixels from window/element's top or percentage of the window/element's height to scroll to.",
153 | },
154 | ],
155 | modalCreateCode: function (args: ModalCreateCodeType): string {
156 | if (args[1] === empty) {
157 | return `.scrollTo('${args[0]}')`;
158 | } else if (args[0] !== empty && args[1] !== empty) {
159 | const value1: string | number = isNaN(Number(args[0]))
160 | ? `'${args[0]}'`
161 | : Number(args[0]);
162 | const value2: string | number = isNaN(Number(args[1]))
163 | ? `'${args[1]}'`
164 | : Number(args[1]);
165 | return `.scrollTo(${value1}, ${value2})`;
166 | } else {
167 | return;
168 | }
169 | },
170 | },
171 | scrollToCy: {
172 | option: "Cy.ScrollTo",
173 | code: "cy.scrollTo()",
174 | tooltip: "Scroll the element to a specific position (Chained Off Cy).",
175 | modal: [
176 | {
177 | type: "label",
178 | labelText: "Scroll the element to a specific position.",
179 | },
180 | {
181 | type: "input",
182 | inputType:
183 | "A specified position to scroll the window or element to. Valid positions are topLeft, top, topRight, left, center, right, bottomLeft, bottom, and bottomRight.",
184 | },
185 | {
186 | type: "input",
187 | inputType:
188 | "(x,y) x: The distance in pixels from window/element's left or percentage of the window/element's width to scroll to. y: the distance in pixels from window/element's top or percentage of the window/element's height to scroll to.",
189 | },
190 | ],
191 | modalCreateCode: function (args: ModalCreateCodeType): string {
192 | if (args[1] === empty) {
193 | return `cy.scrollTo('${args[0]}')`;
194 | } else if (args[0] !== empty && args[1] !== empty) {
195 | const value1: string | number = isNaN(Number(args[0]))
196 | ? `'${args[0]}'`
197 | : Number(args[0]);
198 | const value2: string | number = isNaN(Number(args[1]))
199 | ? `'${args[1]}'`
200 | : Number(args[1]);
201 | return `cy.scrollTo(${value1}, ${value2})`;
202 | } else {
203 | return;
204 | }
205 | },
206 | },
207 | select: {
208 | option: "Select",
209 | code: ".select()",
210 | tooltip: "Select an option from a dropdown.",
211 | modal: [
212 | { type: "label", labelText: "Select an option from a dropdown." },
213 | {
214 | type: "input",
215 | inputType:
216 | "The value, index, or text content of the to be selected.",
217 | },
218 | ],
219 | modalCreateCode: function (args: ModalCreateCodeType): string {
220 | if (args[0] === empty) {
221 | return `.select()`;
222 | } else {
223 | const value1: string | number = isNaN(Number(args[0]))
224 | ? `'${args[0]}'`
225 | : Number(args[0]);
226 | return `.select(${value1})`;
227 | }
228 | },
229 | },
230 | selectFile: {
231 | option: "Select File",
232 | code: ".selectFile()",
233 | tooltip: "Select a file to upload.",
234 | modal: [
235 | { type: "label", labelText: "Select a file to upload." },
236 | { type: "input", inputType: "A path to a file within the project root" },
237 | ],
238 | modalCreateCode: function (args: ModalCreateCodeType): string {
239 | if (args[0]) {
240 | return `.selectFile('${args[0]}')`;
241 | } else {
242 | return;
243 | }
244 | },
245 | },
246 | trigger: {
247 | option: "Trigger",
248 | code: ".trigger()",
249 | tooltip: "Trigger an event on the element.",
250 | modal: [
251 | { type: "label", labelText: "Trigger an event on the element." },
252 | {
253 | type: "input",
254 | inputType: "The name of the event to be triggered on the DOM element.",
255 | },
256 | {
257 | type: "input",
258 | inputType:
259 | "OPTIONAL: The distance in pixels from element's left to trigger the event.",
260 | },
261 | {
262 | type: "input",
263 | inputType:
264 | "OPTIONAL: The distance in pixels from element's top to trigger the event.",
265 | },
266 | ],
267 | modalCreateCode: function (args: ModalCreateCodeType): string {
268 | if (args[0] && args[1] === empty && args[2] === empty) {
269 | return `.trigger('${args[0]}')`;
270 | } else if (args[0] !== empty && args[1] !== empty && args[2] === empty) {
271 | return `.trigger('${args[0]}', '${args[1]}')`;
272 | } else if (args[0] !== empty && args[1] !== empty && args[2] !== empty) {
273 | const value1: string | number = isNaN(Number(args[1]))
274 | ? `'${args[1]}'`
275 | : Number(args[1]);
276 | const value2: string | number = isNaN(Number(args[2]))
277 | ? `'${args[2]}'`
278 | : Number(args[2]);
279 | return `.trigger('${args[0]}', ${value1}, ${value2})`;
280 | } else {
281 | return;
282 | }
283 | },
284 | },
285 | type: {
286 | option: "Type",
287 | code: ".type()",
288 | tooltip: "Type text into the input field.",
289 | modal: [
290 | { type: "label", labelText: "Type text into the input field." },
291 | {
292 | type: "input",
293 | inputType: "The text to be typed into the DOM element.",
294 | },
295 | ],
296 | modalCreateCode: function (text: ModalCreateCodeType): string {
297 | if (text[0]) {
298 | return `.type('${text[0]}')`;
299 | } else {
300 | return;
301 | }
302 | },
303 | },
304 | uncheck: {
305 | option: "UnCheck",
306 | code: ".uncheck()",
307 | tooltip: "Uncheck a checkbox or radio button.",
308 | modal: [
309 | { type: "label", labelText: "Text to type" },
310 | {
311 | type: "input",
312 | inputType: "OPTIONAL: Value of checkbox that should be unchecked.",
313 | },
314 | ],
315 | modalCreateCode: function (args: ModalCreateCodeType): string {
316 | if (args[0] === empty) {
317 | return `.uncheck()`;
318 | } else {
319 | return `.uncheck('${args[0]}')`;
320 | }
321 | },
322 | },
323 | };
324 |
325 | export default actionOptions;
326 |
--------------------------------------------------------------------------------
/my-app/src/options/assertionOptions.ts:
--------------------------------------------------------------------------------
1 | import { empty, commonAssertions } from "./optionVariables";
2 |
3 | type ModalCreateCodeType = (string | number)[];
4 |
5 | const assertionOptions = {
6 | should: {
7 | option: "Should",
8 | code: ".should()",
9 | tooltip: "Assert element state or value.",
10 | modal: [
11 | { type: "label", labelText: "Assert element state or value." },
12 | { type: "select", options: commonAssertions },
13 | {
14 | type: "input",
15 | inputType: "OPTIONAL: Value to assert against chainer.",
16 | },
17 | {
18 | type: "input",
19 | inputType: "OPTIONAL: A method to be called on the chainer.",
20 | },
21 | ],
22 | modalCreateCode: function (args: ModalCreateCodeType): string {
23 | if (args[1] === empty && args[2] === empty) {
24 | return `.should('${args[0]}')`;
25 | } else if (args[2] === empty) {
26 | const value1: string | number = isNaN(Number(args[1]))
27 | ? `'${args[1]}'`
28 | : Number(args[1]);
29 | return `.should('${args[0]}', ${value1})`;
30 | } else if (args[0] && args[1] && args[2]) {
31 | const value1: string | number = isNaN(Number(args[1]))
32 | ? `'${args[1]}'`
33 | : Number(args[1]);
34 | const value2: string | number = isNaN(Number(args[2]))
35 | ? `'${args[2]}'`
36 | : Number(args[2]);
37 | return `.should('${args[0]}', ${value1}, ${value2})`;
38 | } else {
39 | return;
40 | }
41 | },
42 | },
43 | and: {
44 | option: "And",
45 | code: ".and()",
46 | tooltip: "Chain additional assertions.",
47 | modal: [
48 | { type: "label", labelText: "Chain additional assertions." },
49 | { type: "select", options: commonAssertions },
50 | {
51 | type: "input",
52 | inputType: "OPTIONAL: Value to assert against chainer.",
53 | },
54 | {
55 | type: "input",
56 | inputType: "OPTIONAL: A method to be called on the chainer.",
57 | },
58 | ],
59 | modalCreateCode: function (args: ModalCreateCodeType): string {
60 | if (args[1] === empty && args[2] === empty) {
61 | return `.and('${args[0]}')`;
62 | } else if (args[2] === empty) {
63 | const value1: string | number = isNaN(Number(args[1]))
64 | ? `'${args[1]}'`
65 | : Number(args[1]);
66 | return `.and('${args[0]}', ${value1})`;
67 | } else if (args[0] && args[1] && args[2]) {
68 | const value1: string | number = isNaN(Number(args[1]))
69 | ? `'${args[1]}'`
70 | : Number(args[1]);
71 | const value2: string | number = isNaN(Number(args[2]))
72 | ? `'${args[2]}'`
73 | : Number(args[2]);
74 | return `.and('${args[0]}', ${value1}, ${value2})`;
75 | } else {
76 | return;
77 | }
78 | },
79 | },
80 | };
81 |
82 | export default assertionOptions;
83 |
--------------------------------------------------------------------------------
/my-app/src/options/optionVariables.ts:
--------------------------------------------------------------------------------
1 | export const empty = '';
2 |
3 | export const commonAssertions = [
4 | 'have.length',
5 | 'not.have.class',
6 | 'have.value',
7 | 'have.text',
8 | 'include.text',
9 | 'not.contain',
10 | 'match',
11 | 'be.visible',
12 | 'not.be.visible',
13 | 'not.exist',
14 | 'be.checked',
15 | 'have.css',
16 | 'not.have.css',
17 | 'be.disabled',
18 | 'not.be.disabled',
19 | 'be-enabled',
20 | 'eq',
21 | ];
22 |
23 | export const encodingArray = [
24 | 'null',
25 | 'ascii',
26 | 'base64',
27 | 'binary',
28 | 'hex',
29 | 'latin1',
30 | 'utf8',
31 | 'utf-8',
32 | 'ucs2',
33 | 'ucs-2',
34 | 'utf16le',
35 | 'utf-16le',
36 | ];
37 |
38 |
--------------------------------------------------------------------------------
/my-app/src/options/otherCommandOptions.ts:
--------------------------------------------------------------------------------
1 | import { empty, encodingArray } from "./optionVariables";
2 |
3 | type ModalCreateCodeType = (string | number)[];
4 |
5 | const otherCommandOptions = {
6 | blur: {
7 | option: "Blur",
8 | code: ".blur()",
9 | tooltip: "Remove focus from the selected element, triggering a blur event.",
10 | },
11 | clearAllCookies: {
12 | option: "Clear All Cookies",
13 | code: "cy.clearAllCookies()",
14 | tooltip: "Clear all cookies in the browser.",
15 | },
16 | clearAllLocalStorage: {
17 | option: "Clear All Local Storage",
18 | code: "cy.clearAllLocalStorage()",
19 | tooltip: "Clear all items from the local storage.",
20 | },
21 | clearAllSessionStorage: {
22 | option: "Clear All Session Storage",
23 | code: "cy.clearAllSessionStorage()",
24 | tooltip: "Clear all items from the session storage.",
25 | },
26 | clearCookie: {
27 | option: "Clear Cookie",
28 | code: "cy.clearCookie()",
29 | tooltip: "Clear a specific cookie by name.",
30 | modal: [
31 | { type: "label", labelText: "Clear a specific cookie by name." },
32 | { type: "input", inputType: "The name of the cookie to be cleared." },
33 | ],
34 | modalCreateCode: function (text: ModalCreateCodeType): string {
35 | if (text[0]) {
36 | return `cy.clearCookie('${text[0]}')`;
37 | } else {
38 | return;
39 | }
40 | },
41 | },
42 | clearCookies: {
43 | option: "Clear Cookies",
44 | code: "cy.clearCookies()",
45 | tooltip: "Clear all cookies in the browser.",
46 | },
47 | clearLocalStorage: {
48 | option: "Clear Local Storage",
49 | code: "cy.clearLocalStorage()",
50 | tooltip: "Clear all items from the local storage.",
51 | modal: [
52 | { type: "label", labelText: "Clear all items from the local storage." },
53 | {
54 | type: "input",
55 | inputType: "OPTIONAL: Specify key to be cleared in localStorage.",
56 | },
57 | ],
58 | modalCreateCode: function (args: ModalCreateCodeType): string {
59 | if (args[0] === empty) {
60 | return `cy.clearLocalStorage()`;
61 | } else {
62 | return `cy.clearLocalStorage('${args[0]}')`;
63 | }
64 | },
65 | },
66 | debug: {
67 | option: "Debug",
68 | code: ".debug()",
69 | tooltip: "Trigger a debug breakpoint in your test (Chained Off Dom El).",
70 | },
71 | debugCy: {
72 | option: "Cy.Debug",
73 | code: "cy.debug()",
74 | tooltip: "Trigger a debug breakpoint in your test (Chained Off Cy).",
75 | },
76 | end: {
77 | option: "End",
78 | code: ".end()",
79 | tooltip: "End the current command chain and return the previous subject.",
80 | },
81 | exec: {
82 | option: "Exec",
83 | code: "cy.exec()",
84 | tooltip: "Execute a system command from within a Cypress test.",
85 | modal: [
86 | {
87 | type: "label",
88 | labelText: "Execute a system command from within a Cypress test.",
89 | },
90 | {
91 | type: "input",
92 | inputType: "The system command to be executed from the project root",
93 | },
94 | ],
95 | modalCreateCode: function (text: ModalCreateCodeType): string {
96 | if (text[0]) {
97 | return `cy.exec('${text[0]}')`;
98 | } else {
99 | return;
100 | }
101 | },
102 | },
103 | fixture: {
104 | option: "Fixture",
105 | code: "cy.fixture()",
106 | tooltip: "Load a fixture file's contents for use in tests.",
107 | modal: [
108 | {
109 | type: "label",
110 | labelText: "Load a fixture file's contents for use in tests.",
111 | },
112 | {
113 | type: "input",
114 | inputType:
115 | "A path to a file within the fixturesFolder , which defaults to cypress/fixtures.",
116 | },
117 | { type: "select", options: encodingArray },
118 | ],
119 | modalCreateCode: function (args: ModalCreateCodeType): string {
120 | if (args[0] !== empty && args[1] === "null") {
121 | return `cy.fixture('${args[0]}')`;
122 | } else if (args[0] !== empty && args[1] !== "null") {
123 | return `cy.fixture('${args[0]}', '${args[1]}')`;
124 | } else {
125 | return;
126 | }
127 | },
128 | },
129 | focus: {
130 | option: "Focus",
131 | code: ".focus()",
132 | tooltip: "Set focus on the selected element.",
133 | },
134 | getAllCookies: {
135 | option: "Get All Cookies",
136 | code: "cy.getAllCookies()",
137 | tooltip: "Retrieve all cookies present in the browser.",
138 | },
139 | getAllLocalStorage: {
140 | option: "Get All Local Storage",
141 | code: "cy.getAllLocalStorage()",
142 | tooltip: "Retrieve all items from the local storage.",
143 | },
144 | getAllSessionStorage: {
145 | option: "Get All Session Storage",
146 | code: "cy.getAllSessionStorage()",
147 | tooltip: "Retrieve all items from the session storage.",
148 | },
149 | getCookie: {
150 | option: "Get Cookie",
151 | code: "cy.getCookie()",
152 | tooltip: "Retrieve the value of a specific cookie by name.",
153 | modal: [
154 | {
155 | type: "label",
156 | labelText: "Retrieve the value of a specific cookie by name.",
157 | },
158 | { type: "input", inputType: "The name of the cookie to get." },
159 | ],
160 | modalCreateCode: function (text: ModalCreateCodeType): string {
161 | if (text[0]) {
162 | return `cy.getCookie('${text[0]}')`;
163 | } else {
164 | return;
165 | }
166 | },
167 | },
168 | getCookies: {
169 | option: "Get Cookies",
170 | code: "cy.getCookies()",
171 | tooltip: "Retrieve all cookies present in the browser.",
172 | },
173 | go: {
174 | option: "Go",
175 | code: ".go()",
176 | tooltip: "Navigate forward or backward in the browser's history.",
177 | modal: [
178 | {
179 | type: "label",
180 | labelText: "Navigate forward or backward in the browser's history.",
181 | },
182 | {
183 | type: "input",
184 | inputType:
185 | "The direction to navigate. You can use back or forward to go one step back or forward. You could also navigate to a specific history position (-1 goes back one page, 1 goes forward one page, etc).",
186 | },
187 | ],
188 | modalCreateCode: function (args: ModalCreateCodeType): string {
189 | const value1: string | number = isNaN(Number(args[0]))
190 | ? `'${args[0]}'`
191 | : Number(args[0]);
192 | if (args[0]) {
193 | return `cy.go(${value1})`;
194 | } else {
195 | return;
196 | }
197 | },
198 | },
199 | log: {
200 | option: "Log",
201 | code: "cy.log()",
202 | tooltip: "Log a message to the Cypress Command Log.",
203 | modal: [
204 | { type: "label", labelText: "Log a message to the Cypress Command Log." },
205 | {
206 | type: "input",
207 | inputType:
208 | "Message to be printed to Cypress Command Log. Accepts a Markdown formatted message.",
209 | },
210 | ],
211 | modalCreateCode: function (text: ModalCreateCodeType): string {
212 | if (text[0]) {
213 | return `cy.log('${text[0]}')`;
214 | } else {
215 | return;
216 | }
217 | },
218 | },
219 | pause: {
220 | option: "Pause",
221 | code: ".pause()",
222 | tooltip:
223 | "Pause the test execution to inspect the application's state (Chained Off Dom el).",
224 | },
225 | pauseCy: {
226 | option: "Pause",
227 | code: "cy.pause()",
228 | tooltip:
229 | "Pause the test execution to inspect the application's state (Chained Off Cy).",
230 | },
231 | reload: {
232 | option: "Reload",
233 | code: "cy.reload()",
234 | tooltip: "Reload the current page or the specified page.",
235 | modal: [
236 | {
237 | type: "label",
238 | labelText: "Reload the current page or the specified page.",
239 | },
240 | { type: "select", options: ["null", "true", "false"] },
241 | ],
242 | modalCreateCode: function (args: ModalCreateCodeType): string {
243 | if (args[0] === "null") {
244 | return `cy.reload()`;
245 | } else {
246 | return `cy.reload(${args[0]})`;
247 | }
248 | },
249 | },
250 | screenshot: {
251 | option: "Screenshot",
252 | code: ".screenshot()",
253 | tooltip:
254 | "Take a screenshot of the current viewport or a specific element (Chained Off Dom El).",
255 | modal: [
256 | {
257 | type: "label",
258 | labelText:
259 | "Take a screenshot of the current viewport or a specific element.",
260 | },
261 | { type: "select", options: ["null", "true", "false"] },
262 | ],
263 | modalCreateCode: function (args: ModalCreateCodeType): string {
264 | if (args[0] === "null") {
265 | return `.screenshot()`;
266 | } else {
267 | return `.screenshot('${args[0]}')`;
268 | }
269 | },
270 | },
271 | screenshotCy: {
272 | option: "Screenshot",
273 | code: "cy.screenshot()",
274 | tooltip:
275 | "Take a screenshot of the current viewport or a specific element (Chained Off Cy).",
276 | modal: [
277 | {
278 | type: "label",
279 | labelText:
280 | "Take a screenshot of the current viewport or a specific element.",
281 | },
282 | { type: "select", options: ["null", "true", "false"] },
283 | ],
284 | modalCreateCode: function (args: ModalCreateCodeType): string {
285 | if (args[0] === "null") {
286 | return `cy.screenshot()`;
287 | } else {
288 | return `cy.screenshot('${args[0]}')`;
289 | }
290 | },
291 | },
292 | setCookie: {
293 | option: "Set Cookie",
294 | code: "cy.setCookie()",
295 | tooltip: "Set a specific cookie with a name and value.",
296 | modal: [
297 | {
298 | type: "label",
299 | labelText: "Set a specific cookie with a name and value.",
300 | },
301 | { type: "input", inputType: "The name of the cookie to set." },
302 | { type: "input", inputType: "The value of the cookie to set." },
303 | ],
304 | modalCreateCode: function (text: ModalCreateCodeType): string {
305 | if (text[0] && text[1]) {
306 | return `cy.setCookie('${text[0]}', '${text[1]}')`;
307 | } else {
308 | return;
309 | }
310 | },
311 | },
312 | submit: {
313 | option: "Submit",
314 | code: ".submit()",
315 | tooltip: "Submit a form element.",
316 | },
317 | tick: {
318 | option: "Tick",
319 | code: "cy.tick()",
320 | tooltip: "Control the Cypress clock to manipulate time in tests.",
321 | modal: [
322 | {
323 | type: "label",
324 | labelText: "Control the Cypress clock to manipulate time in tests.",
325 | },
326 | {
327 | type: "input",
328 | inputType:
329 | "The number of milliseconds to move the clock. Any timers within the affected range of time will be called.",
330 | },
331 | ],
332 | modalCreateCode: function (text: ModalCreateCodeType): string {
333 | if (text[0]) {
334 | return `cy.tick(${text[0]})`;
335 | } else {
336 | return;
337 | }
338 | },
339 | },
340 | viewport: {
341 | option: "Viewport",
342 | code: "cy.viewport()",
343 | tooltip: "Set the dimensions of the browser's viewport.",
344 | modal: [
345 | {
346 | type: "label",
347 | labelText: "Set the dimensions of the browser's viewport.",
348 | },
349 | {
350 | type: "input",
351 | inputType:
352 | "Width of viewport in pixels (must be a non-negative, finite number).",
353 | },
354 | {
355 | type: "input",
356 | inputType:
357 | "Height of viewport in pixels (must be a non-negative, finite number).",
358 | },
359 | ],
360 | modalCreateCode: function (args: ModalCreateCodeType): string {
361 | if (args[0] !== empty && args[1] !== empty) {
362 | const value1: string | number = isNaN(Number(args[0]))
363 | ? `'${args[0]}'`
364 | : Number(args[0]);
365 | const value2: string | number = isNaN(Number(args[1]))
366 | ? `'${args[1]}'`
367 | : Number(args[1]);
368 | return `cy.viewport(${value1}, ${value2})`;
369 | } else {
370 | return;
371 | }
372 | },
373 | },
374 | visit: {
375 | option: "Visit",
376 | code: "cy.visit()",
377 | tooltip: "Navigate to a specific URL.",
378 | modal: [
379 | { type: "label", labelText: "Navigate to a specific URL." },
380 | { type: "input", inputType: "The URL to visit." },
381 | ],
382 | modalCreateCode: function (text: ModalCreateCodeType): string {
383 | if (text[0]) {
384 | return `cy.visit('${text[0]}')`;
385 | } else {
386 | return;
387 | }
388 | },
389 | },
390 | wait: {
391 | option: "Wait",
392 | code: "cy.wait()",
393 | tooltip:
394 | "Pause the test to wait for a specific amount of time or until a specific event occurs.",
395 | modal: [
396 | {
397 | type: "label",
398 | labelText:
399 | "Pause the test to wait for a specific amount of time or until a specific event occurs.",
400 | },
401 | {
402 | type: "input",
403 | inputType: "The amount of time to wait in milliseconds.",
404 | },
405 | ],
406 | modalCreateCode: function (args: ModalCreateCodeType): string {
407 | if (args[0] !== empty && args[1] !== empty) {
408 | const value1: string | number = isNaN(Number(args[0]))
409 | ? `'${args[0]}'`
410 | : Number(args[0]);
411 | return `cy.wait(${value1})`;
412 | } else {
413 | return;
414 | }
415 | },
416 | },
417 | writeFile: {
418 | option: "Write File",
419 | code: "cy.writeFile()",
420 | tooltip: "Write content to a file with optional encoding.",
421 | modal: [
422 | {
423 | type: "label",
424 | labelText: "Write content to a file with optional encoding.",
425 | },
426 | { type: "input", inputType: "A path to a file within the project root" },
427 | {
428 | type: "input",
429 | inputType: "The contents to be written to the file.xt",
430 | },
431 | { type: "select", options: encodingArray },
432 | ],
433 | modalCreateCode: function (args: ModalCreateCodeType): string {
434 | if (args[0] !== empty && args[1] !== empty && args[2] === "null") {
435 | return `cy.writeFile('${args[0]}', '${args[1]}')`;
436 | } else if (args[0] !== empty && args[1] !== empty && args[2] !== "null") {
437 | return `cy.writeFile('${args[0]}', '${args[1]}', '${args[2]}')`;
438 | } else {
439 | return;
440 | }
441 | },
442 | },
443 | };
444 |
445 | export default otherCommandOptions;
446 |
--------------------------------------------------------------------------------
/my-app/src/parser.ts:
--------------------------------------------------------------------------------
1 | import * as babelParser from "@babel/parser";
2 | import * as fs from "fs";
3 | import * as path from "path";
4 | import { Tree } from "./types/Tree";
5 | import { ImportObj } from "./types/ImportObj";
6 | import { File } from "@babel/types";
7 | import { getNonce } from "./getNonce";
8 |
9 | export class Parser {
10 | entryFile: string;
11 | tree: Tree | undefined;
12 |
13 | constructor(filePath: string) {
14 | // Fix when selecting files in WSL file system
15 | this.entryFile = filePath;
16 | if (process.platform === "linux" && this.entryFile.includes("wsl$")) {
17 | this.entryFile = path.resolve(
18 | filePath.split(path.win32.sep).join(path.posix.sep)
19 | );
20 | this.entryFile = "/" + this.entryFile.split("/").slice(3).join("/");
21 | // Fix for when running WSL but selecting files held on Windows file system
22 | } else if (
23 | process.platform === "linux" &&
24 | /[a-zA-Z]/.test(this.entryFile[0])
25 | ) {
26 | const root = `/mnt/${this.entryFile[0].toLowerCase()}`;
27 | this.entryFile = path.join(
28 | root,
29 | filePath.split(path.win32.sep).slice(1).join(path.posix.sep)
30 | );
31 | }
32 |
33 | this.tree = undefined;
34 | }
35 |
36 | // Public method to generate component tree based on current entryFile
37 | public parse(): Tree {
38 | // Create root Tree node
39 | const root: Tree = {
40 | id: getNonce(),
41 | name: path.basename(this.entryFile).replace(/\.(t|j)sx?$/, ""),
42 | fileName: path.basename(this.entryFile),
43 | filePath: this.entryFile,
44 | importPath: "/", // this.entryFile here breaks windows file path on root e.g. C:\\ is detected as third party
45 | expanded: false,
46 | depth: 0,
47 | count: 1,
48 | thirdParty: false,
49 | reactRouter: false,
50 | reduxConnect: false,
51 | htmlChildrenTestIds: "",
52 | children: [],
53 | parentList: [],
54 | props: {},
55 | error: "",
56 | };
57 |
58 | this.tree = root;
59 | this.parser(root);
60 | return this.tree;
61 | }
62 |
63 | public getTree(): Tree {
64 | return this.tree!;
65 | }
66 |
67 | // Set Sapling Parser with a specific Data Tree (from workspace state)
68 | public setTree(tree: Tree): void {
69 | this.entryFile = tree.filePath;
70 | this.tree = tree;
71 | }
72 |
73 | public updateTree(filePath: string): Tree {
74 | let children: any[] = [];
75 |
76 | const getChildNodes = (node: Tree): void => {
77 | const { depth, filePath, expanded } = node;
78 | children.push({ depth, filePath, expanded });
79 | };
80 |
81 | const matchExpand = (node: Tree): void => {
82 | for (let i = 0; i < children.length; i += 1) {
83 | const oldNode = children[i];
84 | if (
85 | oldNode.depth === node.depth &&
86 | oldNode.filePath === node.filePath &&
87 | oldNode.expanded
88 | ) {
89 | node.expanded = true;
90 | }
91 | }
92 | };
93 |
94 | const callback = (node: Tree): void => {
95 | if (node.filePath === filePath) {
96 | node.children.forEach((child: Tree) => {
97 | this.traverseTree(getChildNodes, child);
98 | });
99 |
100 | const newNode = this.parser(node);
101 |
102 | this.traverseTree(matchExpand, newNode);
103 |
104 | children = [];
105 | }
106 | };
107 |
108 | this.traverseTree(callback, this.tree);
109 |
110 | return this.tree!;
111 | }
112 |
113 | // Traverses the tree and changes expanded property of node whose id matches provided id
114 | public toggleNode(id: string, expanded: boolean): Tree {
115 | const callback = (node: { id: string; expanded: boolean }) => {
116 | if (node.id === id) {
117 | node.expanded = expanded;
118 | }
119 | };
120 |
121 | this.traverseTree(callback, this.tree);
122 |
123 | return this.tree!;
124 | }
125 |
126 | // Traverses all nodes of the current component tree and applies callback to each node
127 | private traverseTree(
128 | callback: (node: Tree) => void,
129 | node: Tree | undefined = this.tree
130 | ): void {
131 | if (!node) {
132 | return;
133 | }
134 |
135 | callback(node);
136 |
137 | node.children.forEach((childNode) => {
138 | this.traverseTree(callback, childNode);
139 | });
140 | }
141 |
142 | // Recursively builds the React component tree structure starting from the root node
143 | private parser(componentTree: Tree): Tree | undefined {
144 | // If import is a node module, do not parse any deeper
145 | if (!["\\", "/", "."].includes(componentTree.importPath[0])) {
146 | componentTree.thirdParty = true;
147 | if (
148 | componentTree.fileName === "react-router-dom" ||
149 | componentTree.fileName === "react-router"
150 | ) {
151 | componentTree.reactRouter = true;
152 | }
153 | return;
154 | }
155 |
156 | // Check that file has a valid fileName/Path, if not found, add an error to the node and halt
157 | const fileName = this.getFileName(componentTree);
158 | if (!fileName) {
159 | componentTree.error = "File not found.";
160 | return;
161 | }
162 |
163 | // If the current node recursively calls itself, do not parse any deeper:
164 | if (componentTree.parentList.includes(componentTree.filePath)) {
165 | return;
166 | }
167 |
168 | // Create an abstract syntax tree of the current component tree file
169 | let ast: babelParser.ParseResult;
170 | try {
171 | ast = babelParser.parse(
172 | fs.readFileSync(path.resolve(componentTree.filePath), "utf-8"),
173 | {
174 | sourceType: "module",
175 | tokens: true,
176 | plugins: ["jsx", "typescript"],
177 | }
178 | );
179 | } catch (err) {
180 | componentTree.error = "Error while processing this file/node";
181 | return componentTree;
182 | }
183 |
184 | // Find imports in the current file, then find child components in the current file
185 | const imports = this.getImports(ast.program.body);
186 | // Get any JSX Children of the current file:
187 | if (ast.tokens) {
188 | componentTree.htmlChildrenTestIds = this.findTestIds(
189 | ast.tokens,
190 | imports,
191 | componentTree
192 | );
193 | }
194 | if (ast.tokens) {
195 | componentTree.children = this.getJSXChildren(
196 | ast.tokens,
197 | imports,
198 | componentTree
199 | );
200 | }
201 |
202 | // Check if the current node is connected to the Redux store
203 | if (ast.tokens) {
204 | componentTree.reduxConnect = this.checkForRedux(ast.tokens, imports);
205 | }
206 |
207 | // Recursively parse all child components
208 | componentTree.children.forEach((child) => this.parser(child));
209 | return componentTree;
210 | }
211 |
212 | // Finds files where the import string does not include a file extension
213 | private getFileName(componentTree: Tree): string | undefined {
214 | const ext = path.extname(componentTree.filePath);
215 | let fileName: string | undefined = componentTree.fileName;
216 |
217 | if (!ext) {
218 | // Try and find a file extension that exists in the directory:
219 | const fileArray = fs.readdirSync(path.dirname(componentTree.filePath));
220 | const regEx = new RegExp(`${componentTree.fileName}.(j|t)sx?$`);
221 | fileName = fileArray.find((fileStr) => fileStr.match(regEx));
222 | fileName ? (componentTree.filePath += path.extname(fileName)) : null;
223 | }
224 |
225 | return fileName;
226 | }
227 |
228 | // Extracts Imports from the current file
229 | // const Page1 = lazy(() => import('./page1')); -> is parsed as 'ImportDeclaration'
230 | // import Page2 from './page2'; -> is parsed as 'VariableDeclaration'
231 | private getImports(body: { [key: string]: any }[]): ImportObj {
232 | const bodyImports = body.filter(
233 | (item) =>
234 | item.type === "ImportDeclaration" || item.type === "VariableDeclaration"
235 | );
236 |
237 | return bodyImports.reduce((accum, curr) => {
238 | // Import Declarations:
239 | if (curr.type === "ImportDeclaration") {
240 | curr.specifiers.forEach(
241 | (i: {
242 | local: { name: string | number };
243 | imported: { name: any };
244 | }) => {
245 | accum[i.local.name] = {
246 | importPath: curr.source.value,
247 | importName: i.imported ? i.imported.name : i.local.name,
248 | };
249 | }
250 | );
251 | }
252 | // Imports Inside Variable Declarations: // Not easy to deal with nested objects
253 | if (curr.type === "VariableDeclaration") {
254 | const importPath = this.findVarDecImports(curr.declarations[0]);
255 | if (importPath) {
256 | const importName = curr.declarations[0].id.name;
257 | accum[curr.declarations[0].id.name] = {
258 | importPath,
259 | importName,
260 | };
261 | }
262 | }
263 | return accum;
264 | }, {});
265 | }
266 |
267 | // Recursive helper method to find the import path in Variable Declaration
268 | private findVarDecImports(ast: { [key: string]: any }): string | boolean {
269 | // Base Case, find the import path in the variable declaration and return it,
270 | if (
271 | Object.prototype.hasOwnProperty.call(ast, "callee") &&
272 | ast.callee.type === "Import"
273 | ) {
274 | return ast.arguments[0].value;
275 | }
276 |
277 | // Otherwise look for imports in any other non-null/undefined objects in the tree:
278 | for (const key in ast) {
279 | if (
280 | Object.prototype.hasOwnProperty.call(ast, key) &&
281 | typeof ast[key] === "object" &&
282 | ast[key]
283 | ) {
284 | const importPath = this.findVarDecImports(ast[key]);
285 | if (importPath) {
286 | return importPath;
287 | }
288 | }
289 | }
290 |
291 | return false;
292 | }
293 |
294 | // Finds html components that have a test-id
295 | private findTestIds(
296 | astTokens: any[],
297 | importsObj: ImportObj,
298 | parentNode: Tree
299 | ): any {
300 | const childNodes: { [key: string]: Tree } = {};
301 | const props: { [key: string]: boolean } = {};
302 | let token: { [key: string]: any };
303 | const validTestId = [];
304 | const differentTestIds = ["data-cy", "data-test", "data-testid"];
305 |
306 | for (let i = 0; i < astTokens.length; i++) {
307 | if (
308 | astTokens[i].type.label === "jsxTagStart" &&
309 | astTokens[i + 1].type.label === "jsxName" &&
310 | !importsObj[astTokens[i + 1].value]
311 | ) {
312 | while (astTokens[i].type.label !== "jsxTagEnd") {
313 | if (
314 | astTokens[i].type.label === "jsxName" &&
315 | differentTestIds.includes(astTokens[i].value) &&
316 | astTokens[i + 1].value === "="
317 | ) {
318 | validTestId.push(
319 | `[${astTokens[i].value}${astTokens[i + 1].value}${
320 | astTokens[i + 2].value
321 | }]`
322 | );
323 | }
324 | i += 1;
325 | }
326 | }
327 | }
328 | return validTestId;
329 | }
330 |
331 | // Finds JSX React Components in the current file
332 | private getJSXChildren(
333 | astTokens: any[],
334 | importsObj: ImportObj,
335 | parentNode: Tree
336 | ): Tree[] {
337 | let childNodes: { [key: string]: Tree } = {};
338 | let props: { [key: string]: boolean } = {};
339 | let token: { [key: string]: any };
340 |
341 | for (let i = 0; i < astTokens.length; i++) {
342 | // Case for finding JSX tags eg
343 | if (
344 | astTokens[i].type.label === "jsxTagStart" &&
345 | astTokens[i + 1].type.label === "jsxName" &&
346 | importsObj[astTokens[i + 1].value]
347 | ) {
348 | token = astTokens[i + 1];
349 | props = this.getJSXProps(astTokens, i + 2);
350 | childNodes = this.getChildNodes(
351 | importsObj,
352 | token,
353 | props,
354 | parentNode,
355 | childNodes
356 | );
357 |
358 | // Case for finding components passed in as props e.g.
359 | } else if (
360 | astTokens[i].type.label === "jsxName" &&
361 | (astTokens[i].value === "component" ||
362 | astTokens[i].value === "children") &&
363 | importsObj[astTokens[i + 3].value]
364 | ) {
365 | token = astTokens[i + 3];
366 | childNodes = this.getChildNodes(
367 | importsObj,
368 | token,
369 | props,
370 | parentNode,
371 | childNodes
372 | );
373 | }
374 | }
375 |
376 | return Object.values(childNodes);
377 | }
378 |
379 | private getChildNodes(
380 | imports: ImportObj,
381 | astToken: { [key: string]: any },
382 | props: { [key: string]: boolean },
383 | parent: Tree,
384 | children: { [key: string]: Tree }
385 | ): { [key: string]: Tree } {
386 | if (children[astToken.value]) {
387 | children[astToken.value].count += 1;
388 | children[astToken.value].props = {
389 | ...children[astToken.value].props,
390 | ...props,
391 | };
392 | } else {
393 | // Add tree node to childNodes if one does not exist
394 | children[astToken.value] = {
395 | id: getNonce(),
396 | name: imports[astToken.value]["importName"],
397 | fileName: path.basename(imports[astToken.value]["importPath"]),
398 | filePath: path.resolve(
399 | path.dirname(parent.filePath),
400 | imports[astToken.value]["importPath"]
401 | ),
402 | importPath: imports[astToken.value]["importPath"],
403 | expanded: false,
404 | depth: parent.depth + 1,
405 | thirdParty: false,
406 | reactRouter: false,
407 | reduxConnect: false,
408 | count: 1,
409 | htmlChildrenTestIds: "",
410 | props: props,
411 | children: [],
412 | parentList: [parent.filePath].concat(parent.parentList),
413 | error: "",
414 | };
415 | }
416 |
417 | return children;
418 | }
419 |
420 | // Extracts prop names from a JSX element
421 | private getJSXProps(
422 | astTokens: { [key: string]: any }[],
423 | j: number
424 | ): { [key: string]: boolean } {
425 | const props: any = {};
426 | while (astTokens[j].type.label !== "jsxTagEnd") {
427 | if (
428 | astTokens[j].type.label === "jsxName" &&
429 | astTokens[j + 1].value === "="
430 | ) {
431 | props[astTokens[j].value] = true;
432 | }
433 | j += 1;
434 | }
435 | return props;
436 | }
437 |
438 | // Checks if the current Node is connected to the React-Redux Store
439 | private checkForRedux(astTokens: any[], importsObj: ImportObj): boolean {
440 | // Check that react-redux is imported in this file (and we have a connect method or otherwise)
441 | let reduxImported = false;
442 | let connectAlias;
443 | Object.keys(importsObj).forEach((key) => {
444 | if (
445 | importsObj[key].importPath === "react-redux" &&
446 | importsObj[key].importName === "connect"
447 | ) {
448 | reduxImported = true;
449 | connectAlias = key;
450 | }
451 | });
452 |
453 | if (!reduxImported) {
454 | return false;
455 | }
456 |
457 | // Check that connect method is invoked and exported in the file
458 | for (let i = 0; i < astTokens.length; i += 1) {
459 | if (
460 | astTokens[i].type.label === "export" &&
461 | astTokens[i + 1].type.label === "default" &&
462 | astTokens[i + 2].value === connectAlias
463 | ) {
464 | return true;
465 | }
466 | }
467 | return false;
468 | }
469 | }
470 |
--------------------------------------------------------------------------------
/my-app/src/preload.ts:
--------------------------------------------------------------------------------
1 | // See the Electron documentation for details on how to use preload scripts:
2 | // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
3 |
--------------------------------------------------------------------------------
/my-app/src/renderer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file will automatically be loaded by webpack and run in the "renderer" context.
3 | * To learn more about the differences between the "main" and the "renderer" context in
4 | * Electron, visit:
5 | *
6 | * https://electronjs.org/docs/latest/tutorial/process-model
7 | *
8 | * By default, Node.js integration in this file is disabled. When enabling Node.js integration
9 | * in a renderer process, please be aware of potential security implications. You can read
10 | * more about security risks here:
11 | *
12 | * https://electronjs.org/docs/tutorial/security
13 | *
14 | * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration`
15 | * flag:
16 | *
17 | * ```
18 | * // Create the browser window.
19 | * mainWindow = new BrowserWindow({
20 | * width: 800,
21 | * height: 600,
22 | * webPreferences: {
23 | * nodeIntegration: true
24 | * }
25 | * });
26 | * ```
27 | */
28 |
29 | import './index.css';
30 | import './main';
31 | console.log(
32 | '👋 This message is being logged by "renderer.js", included via webpack',
33 | );
34 |
--------------------------------------------------------------------------------
/my-app/src/types/ImportObj.ts:
--------------------------------------------------------------------------------
1 | export type ImportObj = {
2 | [key: string]: { importPath: string; importName: string };
3 | };
4 |
--------------------------------------------------------------------------------
/my-app/src/types/Tree.ts:
--------------------------------------------------------------------------------
1 | // React component tree is a nested data structure, children are Trees
2 |
3 | export type Tree = {
4 | id: string;
5 | name: string;
6 | fileName: string;
7 | filePath: string;
8 | importPath: string;
9 | expanded: boolean;
10 | depth: number;
11 | count: number;
12 | thirdParty: boolean;
13 | reactRouter: boolean;
14 | reduxConnect: boolean;
15 | children: Tree[];
16 | htmlChildrenTestIds: any;
17 | parentList: string[];
18 | props: { [key: string]: boolean };
19 | error: string;
20 | };
21 |
--------------------------------------------------------------------------------
/my-app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
4 | important: true,
5 | theme: {
6 | extend: {
7 | colors:{
8 | secondary: "#14161d",
9 | primary: "#1DF28F",
10 | primaryDark: "#0bc06c",
11 | secondaryPrimary: "#048C7F",
12 | secondaryPrimaryDark: "#037066"
13 | },
14 | backgroundImage: {
15 | logo: "url('/logo.png')",
16 | },
17 | },
18 | },
19 | plugins: [],
20 | };
21 |
--------------------------------------------------------------------------------
/my-app/testing/test2.js:
--------------------------------------------------------------------------------
1 | import { describe, it, before, after } from 'mocha';
2 | import { expect } from 'chai';
3 | import { Builder } from 'selenium-webdriver';
4 |
5 | describe('Landing Page', function() {
6 | let driver;
7 |
8 | before(async function() {
9 | driver = await new Builder()
10 | .forBrowser('electron')
11 | .usingServer('http://localhost:9515') // Electron-Chromedriver’s port
12 | .build();
13 | });
14 |
15 | after(async function() {
16 | await driver.quit();
17 | });
18 |
19 | it('should disable the Next button if no input is provided', async function() {
20 | await driver.get('file:///path-to-your/electron-app.html');
21 | const nextButton = await driver.findElement({ css: 'button' });
22 | expect(await nextButton.isEnabled()).to.be.false;
23 | });
24 |
25 | it('should enable the Next button when input is provided', async function() {
26 | // Simulate file input and port input
27 | await driver.findElement({ css: 'input[type=text]' }).sendKeys('3000');
28 | // Assume GetFile triggers a change in fileTree.name when a file is selected
29 | // Enable the Next button via your logic
30 | const nextButton = await driver.findElement({ css: 'button' });
31 | expect(await nextButton.isEnabled()).to.be.true;
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/my-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "allowJs": true,
5 | "module": "commonjs",
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "noImplicitAny": true,
9 | "sourceMap": true,
10 | "baseUrl": ".",
11 | "outDir": "dist",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "jsx": "react-jsx",
15 | "paths": {
16 | "*": ["node_modules/*"]
17 | }
18 | },
19 | "include": ["src/**/*"]
20 | }
21 |
--------------------------------------------------------------------------------
/my-app/webpack.main.config.ts:
--------------------------------------------------------------------------------
1 | import type { Configuration } from 'webpack';
2 |
3 | import { rules } from './webpack.rules';
4 |
5 | export const mainConfig: Configuration = {
6 | target: 'electron-main',
7 | /**
8 | * This is the main entry point for your application, it's the first file
9 | * that runs in the main process.
10 | */
11 | entry: './src/index.ts',
12 | // Put your normal webpack config below here
13 | module: {
14 | rules,
15 | },
16 | performance: {
17 | hints: false,
18 | },
19 | resolve: {
20 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
21 | },
22 | watchOptions: {
23 | ignored: /UserTests/
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/my-app/webpack.plugins.ts:
--------------------------------------------------------------------------------
1 | import type IForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
2 | import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin';
3 |
4 | // eslint-disable-next-line @typescript-eslint/no-var-requires
5 | const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
6 |
7 |
8 | export const plugins = [
9 | new ForkTsCheckerWebpackPlugin({
10 | logger: 'webpack-infrastructure',
11 | }),
12 | new MonacoWebpackPlugin({
13 | languages: ['javascript']
14 | }),
15 | ];
16 |
--------------------------------------------------------------------------------
/my-app/webpack.renderer.config.ts:
--------------------------------------------------------------------------------
1 | import type { Configuration } from 'webpack';
2 |
3 | import { rules } from './webpack.rules';
4 | import { plugins } from './webpack.plugins';
5 |
6 | export const rendererConfig: Configuration = {
7 | target: 'electron-renderer',
8 | module: {
9 | rules,
10 | },
11 | plugins,
12 | resolve: {
13 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
14 | },
15 | watchOptions: {
16 | ignored: /UserTests/
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/my-app/webpack.rules.ts:
--------------------------------------------------------------------------------
1 | import type { ModuleOptions } from 'webpack';
2 |
3 | export const rules: Required['rules'] = [
4 | // Add support for native node modules
5 | {
6 | // We're specifying native_modules in the test because the asset relocator loader generates a
7 | // "fake" .node file which is really a cjs file.
8 | test: /native_modules[/\\].+\.node$/,
9 | use: 'node-loader',
10 | },
11 | {
12 | test: /[/\\]node_modules[/\\].+\.(m?js|node)$/,
13 | parser: { amd: false },
14 | use: {
15 | loader: '@vercel/webpack-asset-relocator-loader',
16 | options: {
17 | outputAssetBase: 'native_modules',
18 | },
19 | },
20 | },
21 | {
22 | test: /\.tsx?$/,
23 | exclude: /(node_modules|\.webpack)/,
24 | use: {
25 | loader: 'ts-loader',
26 | options: {
27 | transpileOnly: true,
28 | },
29 | },
30 | },
31 | {
32 | // loads .css files
33 | test: /\.css$/,
34 | use: ['style-loader', 'css-loader', 'postcss-loader'],
35 | },
36 | {
37 | test: /\.ttf$/,
38 | type: 'asset/resource'
39 | },
40 | ];
41 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Cydekick",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "devDependencies": {
8 | "@types/node": "^20.6.3"
9 | }
10 | },
11 | "node_modules/@types/node": {
12 | "version": "20.7.1",
13 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.1.tgz",
14 | "integrity": "sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg==",
15 | "dev": true
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@types/node": "^20.6.3"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------