From 4eda1bb6038c15cb47b4c36c6ea4d674b3feb07a Mon Sep 17 00:00:00 2001 From: Pat O'Connor Date: Sat, 22 Mar 2025 22:47:29 +0000 Subject: [PATCH] task(GUIDEFRAME-52): adding docs to site Signed-off-by: Pat O'Connor --- .DS_Store | Bin 10244 -> 8196 bytes docs/README.md | 174 ------------------- docs/assembly.md | 112 ++++++++++++ docs/audio.md | 75 ++++++++ docs/getting-started.md | 42 +++++ docs/guideframe-action-example.md | 49 ++++++ docs/guideframe-md-example.md | 78 +++++++++ docs/guideframe-py-example.md | 192 ++++++++++++++++++++ docs/index.md | 36 +--- docs/installation.md | 47 +++++ docs/library.md | 9 + docs/samples.md | 10 ++ docs/selenium.md | 280 ++++++++++++++++++++++++++++++ docs/utils.md | 101 +++++++++++ docs/video.md | 46 +++++ 15 files changed, 1050 insertions(+), 201 deletions(-) delete mode 100644 docs/README.md create mode 100644 docs/assembly.md create mode 100644 docs/audio.md create mode 100644 docs/getting-started.md create mode 100644 docs/guideframe-action-example.md create mode 100644 docs/guideframe-md-example.md create mode 100644 docs/guideframe-py-example.md create mode 100644 docs/installation.md create mode 100644 docs/library.md create mode 100644 docs/samples.md create mode 100644 docs/selenium.md create mode 100644 docs/utils.md create mode 100644 docs/video.md diff --git a/.DS_Store b/.DS_Store index 6d0c99511a49c34c0a36f09a7a9ecfd2b0c1fcd2..ac6da7cc9200808cc0d52a5d327f0edab7ce967f 100644 GIT binary patch delta 97 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD7i6UH$S7~W*&i9rpF$ lfzm)A!3`u_K`J*EerKM{uM)_?2+_|lIi6?M=2fCh%m5ix5ZV9$ delta 240 zcmZp1XbF&DU|?W$DortDU{C-uIe-{M3-C-V6q~50$SA!rU^hRb^kyD`SSDR5hIEEf zhD?SOhE#?$h9ZVUhFl;k9?Z{W$Op2F7>t291xOl9-XJD6SwNiCyzW0308M3JAZ&sq z&_qL!`I8-lBv~DSuB+P|C$y4vV?zYHAT!ty1_f>)?Fw?`#=`H+llf%=MVKIN*8nMC aWMD7=(US{gdYSUOCOe49Z>|z~!~_5VA29O( diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 1f3b392..0000000 --- a/docs/README.md +++ /dev/null @@ -1,174 +0,0 @@ -# just-the-docs-template - -This is a *bare-minimum* template to create a [Jekyll] site that: - -- uses the [Just the Docs] theme; -- can be built and published on [GitHub Pages]; -- can be built and previewed locally, and published on other platforms. - -More specifically, the created site: - -- uses a gem-based approach, i.e. uses a `Gemfile` and loads the `just-the-docs` gem; -- uses the [GitHub Pages / Actions workflow] to build and publish the site on GitHub Pages. - -To get started with creating a site, simply: - -1. click "[use this template]" to create a GitHub repository -2. go to Settings > Pages > Build and deployment > Source, and select GitHub Actions - -If you want to maintain your docs in the `docs` directory of an existing project repo, see [Hosting your docs from an existing project repo](#hosting-your-docs-from-an-existing-project-repo). - -After completing the creation of your new site on GitHub, update it as needed: - -## Replace the content of the template pages - -Update the following files to your own content: - -- `index.md` (your new home page) -- `README.md` (information for those who access your site repo on GitHub) - -## Changing the version of the theme and/or Jekyll - -Simply edit the relevant line(s) in the `Gemfile`. - -## Adding a plugin - -The Just the Docs theme automatically includes the [`jekyll-seo-tag`] plugin. - -To add an extra plugin, you need to add it in the `Gemfile` *and* in `_config.yml`. For example, to add [`jekyll-default-layout`]: - -- Add the following to your site's `Gemfile`: - - ```ruby - gem "jekyll-default-layout" - ``` - -- And add the following to your site's `_config.yml`: - - ```yaml - plugins: - - jekyll-default-layout - ``` - -Note: If you are using a Jekyll version less than 3.5.0, use the `gems` key instead of `plugins`. - -## Publishing your site on GitHub Pages - -1. If your created site is `YOUR-USERNAME/YOUR-SITE-NAME`, update `_config.yml` to: - - ```yaml - title: YOUR TITLE - description: YOUR DESCRIPTION - theme: just-the-docs - - url: https://YOUR-USERNAME.github.io/YOUR-SITE-NAME - - aux_links: # remove if you don't want this link to appear on your pages - Template Repository: https://github.com/YOUR-USERNAME/YOUR-SITE-NAME - ``` - -2. Push your updated `_config.yml` to your site on GitHub. - -3. In your newly created repo on GitHub: - - go to the `Settings` tab -> `Pages` -> `Build and deployment`, then select `Source`: `GitHub Actions`. - - if there were any failed Actions, go to the `Actions` tab and click on `Re-run jobs`. - -## Building and previewing your site locally - -Assuming [Jekyll] and [Bundler] are installed on your computer: - -1. Change your working directory to the root directory of your site. - -2. Run `bundle install`. - -3. Run `bundle exec jekyll serve` to build your site and preview it at `localhost:4000`. - - The built site is stored in the directory `_site`. - -## Publishing your built site on a different platform - -Just upload all the files in the directory `_site`. - -## Customization - -You're free to customize sites that you create with this template, however you like! - -[Browse our documentation][Just the Docs] to learn more about how to use this theme. - -## Hosting your docs from an existing project repo - -You might want to maintain your docs in an existing project repo. Instead of creating a new repo using the [just-the-docs template](https://github.com/just-the-docs/just-the-docs-template), you can copy the template files into your existing repo and configure the template's Github Actions workflow to build from a `docs` directory. You can clone the template to your local machine or download the `.zip` file to access the files. - -### Copy the template files - -1. Create a `.github/workflows` directory at your project root if your repo doesn't already have one. Copy the `pages.yml` file into this directory. GitHub Actions searches this directory for workflow files. - -2. Create a `docs` directory at your project root and copy all remaining template files into this directory. - -### Modify the GitHub Actions workflow - -The GitHub Actions workflow that builds and deploys your site to Github Pages is defined by the `pages.yml` file. You'll need to edit this file to that so that your build and deploy steps look to your `docs` directory, rather than the project root. - -1. Set the default `working-directory` param for the build job. - - ```yaml - build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: docs - ``` - -2. Set the `working-directory` param for the Setup Ruby step. - - ```yaml - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - cache-version: 0 - working-directory: '${{ github.workspace }}/docs' - ``` - -3. Set the path param for the Upload artifact step: - - ```yaml - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: docs/_site/ - ``` - -4. Modify the trigger so that only changes within the `docs` directory start the workflow. Otherwise, every change to your project (even those that don't affect the docs) would trigger a new site build and deploy. - - ```yaml - on: - push: - branches: - - "main" - paths: - - "docs/**" - ``` - -## Licensing and Attribution - -This repository is licensed under the [MIT License]. You are generally free to reuse or extend upon this code as you see fit; just include the original copy of the license (which is preserved when you "make a template"). While it's not necessary, we'd love to hear from you if you do use this template, and how we can improve it for future use! - -The deployment GitHub Actions workflow is heavily based on GitHub's mixed-party [starter workflows]. A copy of their MIT License is available in [actions/starter-workflows]. - ----- - -[^1]: [It can take up to 10 minutes for changes to your site to publish after you push the changes to GitHub](https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/creating-a-github-pages-site-with-jekyll#creating-your-site). - -[Jekyll]: https://jekyllrb.com -[Just the Docs]: https://just-the-docs.github.io/just-the-docs/ -[GitHub Pages]: https://docs.github.com/en/pages -[GitHub Pages / Actions workflow]: https://github.blog/changelog/2022-07-27-github-pages-custom-github-actions-workflows-beta/ -[Bundler]: https://bundler.io -[use this template]: https://github.com/just-the-docs/just-the-docs-template/generate -[`jekyll-default-layout`]: https://github.com/benbalter/jekyll-default-layout -[`jekyll-seo-tag`]: https://jekyll.github.io/jekyll-seo-tag -[MIT License]: https://en.wikipedia.org/wiki/MIT_License -[starter workflows]: https://github.com/actions/starter-workflows/blob/main/pages/jekyll.yml -[actions/starter-workflows]: https://github.com/actions/starter-workflows/blob/main/LICENSE diff --git a/docs/assembly.md b/docs/assembly.md new file mode 100644 index 0000000..a740397 --- /dev/null +++ b/docs/assembly.md @@ -0,0 +1,112 @@ +--- +title: Assembly +layout: default +nav_order: 5 +parent: Library +permalink: assembly +--- + +# Assembly +The assembly file contains the functions which act to combine the various files created during the GuideFrame pipeline. The following section will list each function contained within this file and provide some insight into its use and syntax. + +### `assemble_audio_video()` +```python +def assemble_audio_video(video_file, audio_file, output_file): + # Check that both files exist + if os.path.exists(video_file) and os.path.exists(audio_file): + # Create video and audio in variables for us in combined output + video_in = ffmpeg.input(video_file) + audio_in = ffmpeg.input(audio_file) + combined_output = ffmpeg.output(video_in, audio_in, output_file, vcodec='copy', acodec='copy') + try: + ( + combined_output.run() + ) + print(f"Successfully created: {output_file}") + # Attempting to extract an error via the wrapper if one occurs + except ffmpeg.Error as e: + # Outputting the error + error_output = e.stderr.decode('utf-8') if e.stderr else "No error details available." + print(f"Error combining {video_file} and {audio_file}: {error_output}") + else: + print(f"Missing video or audio file: {video_file}, {audio_file}") +``` +This function takes a `video_file`, `audio_file` and `output_file` as arguments. It checks for these files before using the `ffmpeg` python package to combine them into a single file, named by the passed argument. This file then contains the combined audio and video for a single `guide_step`. + +### `combine_all_videos()` +```python +def combine_all_videos(output_files, final_output): + # Temp text file to iterate through + file_list = "file_list.txt" + + # Write the list of video files to the same file + with open(file_list, "w") as f: + for video in output_files: + if os.path.exists(video): + f.write(f"file '{video}'\n") + else: + print(f"Error: {video} not found") + + try: + # Run FFmpeg using the concat method and check for errors etc + ffmpeg.input(file_list, format="concat", safe=0).output(final_output, vcodec='libxvid', acodec='aac').run() + print(f"Successfully combined all videos into {final_output}") + except ffmpeg.Error as e: + error_output = e.stderr.decode('utf-8') if e.stderr else "No error details available." + print(f"Error combining videos: {error_output}") + finally: + # Removing the txt file + os.remove(file_list) +``` +This function takes `output_files` and `final_output` as arguments. It takes the array of passed files and writes them to a newly created text file called `file_list`. The `concat` function from `ffmpeg` is used with the `file_list` passed. This is the `input` portion of the command before the `output` portion uses the `final_output` name for outputted file name. + +### `assemble()` +```python +def assemble(number_of_steps): + # Combine individual video and audio for each step by iterating through files and passing to above functions + clip_number = number_of_steps + 1 + for i in range(1, clip_number): + video_file = f"step{i}.mp4" + audio_file = f"step{i}.mp3" + output_file = f"output_step{i}.mp4" + # Call the function to combine video and audio + assemble_audio_video(video_file, audio_file, output_file) + + # Now that all video/audio combinations are complete, combine the output videos into the final one + output_files = [f"output_step{i}.mp4" for i in range(1, clip_number)] + # Now using the extract_script_name function from guideframe_utils.py to get the script name + script_name = extract_script_name() + # Creating a unique filename for the final output file via uuid and the extracted script name + output_filename = f"{script_name}_{uuid.uuid4().hex[:6]}.mp4" + # Combining all the videos into the final output as a single video + combine_all_videos(output_files, output_filename) + + # Check if final_output exists and if so, clean up temporary files (the various mp3 and mp4 files we created) + if os.path.exists(output_filename): + print("Final output created. Cleaning up temporary files...") + # Cleanup loop + for i in range(1, clip_number): + step_video = f"step{i}.mp4" + step_audio = f"step{i}.mp3" + output_step = f"output_step{i}.mp4" + if os.path.exists(step_video): + os.remove(step_video) + print(f"Removed {step_video}") + if os.path.exists(step_audio): + os.remove(step_audio) + if os.path.exists(output_step): + os.remove(output_step) + print(f"Removed {output_step}") + print("Cleanup complete.") + else: + print("Final output not found. No cleanup performed.") +``` +This function uses the two others from this file to perform the overall assembly and cleanup of GuideFrame step files. It takes the `clip_number` argument and uses it to iterate through a loop of all audio and video files in addition to creating the appropriately named `output_file` during the loop. + +*Note: 1 is added to account for steps starting at 1. This means an iteration from 1 -> `clip_number` will have the intended range.* + +Within each loop, these files are passed to the `assemble_audio_video()` function for combination. + +An array of the files outputted by this process is then created using another loop and the `clip_number` variable. A `script_name` variable is intialised using `extract_script_name()` from `utils`. An output file is then created using the `script_name` and a randomly generated `uuid`. The array and filename are then passed to `combine_all_videos()` for final assembly. + +Once this process is complete, a check that a matching file exists is performed. Provided this is successful, a cleanup loop occurs using the `clip_number` variable again to iterate through all created audio and video files with the exception of the final output. \ No newline at end of file diff --git a/docs/audio.md b/docs/audio.md new file mode 100644 index 0000000..2f1048d --- /dev/null +++ b/docs/audio.md @@ -0,0 +1,75 @@ +--- +title: Audio +layout: default +nav_order: 3 +parent: Library +permalink: audio +--- + +# Audio +The audio file contains functions designed to provide the voiceover for each GuideFrame step. It interacts with both `gTTS` and markdown in order to create these mp3 files. The following section will list each function contained within this file and provide some insight into its use and syntax. + +### `export_gtts()` +```python +def export_gtts(text, file_name): + tts = gTTS(text) + tts.save(file_name) + print("Exported", file_name) +``` +This function uses the `gTTS` python package in order to generate audio based on the user-prescribed text. It takes the `text` argument and passes it, along with a `file_name` to the native `gTTS` functions. This then writes an audio file, with the passed name and featuring the prescribed text, to the local directory. + +### `sleep_based_on_vo()` +```python + def sleep_based_on_vo(file_name): + audio = MP3(file_name) + print("Sleeping for", audio.info.length, "seconds") + time.sleep(audio.info.length) +``` +This function is designed to prevent the main script's interactions from accelerating beyond the recorded voiceover. It achieves this by taking the `file_name` of the .mp3 file created during the above function. It then parses the length of this audio file before using the `sleep` function from the `time` package to sleep based on the length found in seconds. This ensures that an interaction cannot occur until the requisite voiceover clip has completed. + +### `pull_vo_from_markdown()` +```python +def pull_vo_from_markdown(md_file, step_number): + # Open the markdown file and read + with open(md_file, "r", encoding="utf-8") as file: + md_content = file.read() + + ''' + Regex pattern breakdown: + + ## Step {step_number} -> The step heading to match + \s* -> Any whitespace characters before the content + (.*?) -> The content under the step heading + (?=\n##|\Z) -> A lookahead to match the next step heading (##) or the end of the file + ''' + + # Define the regex pattern for the step heading (explained above) + step_heading = rf"## Step {step_number}\s*(.*?)\s*(?=\n##|\Z)" + + # Search the markdown content for the step heading + match = re.search(step_heading, md_content, re.DOTALL) + + # Return the content under the step heading if found + return match.group(1).strip() if match else None +``` +This function takes the `md_file` and `step_number` as arguments. It uses these to extract the text content of the markdown file by opening it and then using the `re` package to perform a regex parse (outlined in above code comments). This pattern ensures that the text must follow a `##` heading with text matching "Step n*". Provided a match is found, it is then returned. + +### `generate_voiceover()` +```python +def generate_voicover(md_file, step_number): + # Extract voiceover text from the .md file + voiceover = pull_vo_from_markdown(md_file, step_number) # parsing the markdown + + # Check if content was found + if not voiceover: + print(f"Warning: No content found for Step {step_number}") + return + + # Export the voiceover to an MP3 file + export_gtts(voiceover, f"step{step_number}.mp3") + # Sleeping based on the length of the voiceover + sleep_based_on_vo(f"step{step_number}.mp3") +``` +This function brings the above functions together. It takes `md_file` and `step_number` as arguments before passing these into a call to `pull_vo_from_markdown()`. It then checks for the presence of resulting voiceover. + +It then uses the `export_gtts()` function where it passes the voiceover created above along with a file name created using the `step_number` variable. Finally, it calls the `sleep_based_on_vo()` function to complete the generation cycle. \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..9a61964 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,42 @@ +--- +title: Getting Started +layout: default +nav_order: 3 +permalink: /getting-started/ +--- + +### Getting Started + +Prerequisites: +* You have installed GuideFrame as per the instructions in the [installation](https://chipspeak.github.io/GuideFrame/installation/) section of these docs. +* You have cloned or forked the GuideFrame template repository located [here](https://github.com/chipspeak/GuideFrame-Template). + + +### GuideFrame Script +The [GuideFrame Script Example](https://chipspeak.github.io/guideframe-py-example/) script serves as a template to get you started. It uses the magento test website as an example but the same functions can be used to interact with hrefs and elements from any website. + +Each `guide_step()` function takes a `step_number` (corresponding to the step in the accompanying markdown), function(s) called with `lambda` and an `order` argument. To illustrate this further, lets use an example from the demo. + +```python + ''' + Step 8 - Hover over the "Yoga Straps" product + ''' + guide_step( + 8, + lambda: hover_over_element(driver, "https://magento.softwaretestingboard.com/set-of-sprite-yoga-straps.html") + order="action-before-vo" + ) +``` + +Within the above example, '8' is passed as the step argument. This always corresponds to the step within the matching markdown file. This text is in turn passed to the audio logic to create the voiceover and match it to the appropriate video step. + +`lambda` allows you to pass a function from the selenium-sdk. A wide range of functions are available within this SDK and are detailed [here](https://chipspeak.github.io/selenium/). + +Finally, each `guide_step` features a default argument for `order`. The default is "action-after-vo" but in the above example we've used the other option to place our voiceover after the interaction. This allows you to experiment with the video's pacing as you see fit. + +### GuideFrame Markdown +The [GuideFrame Markdown Example](https://chipspeak.github.io/guideframe-md-example/) should serve to highlight the simplicity of this portion of GuideFrame. A user need only create a `## Step n` heading and place the text for that step underneath. If this format is adhered to, it will be detected appropriately by GuideFrame and used to create the voiceover. + +### GuideFrame Workflow +The [GuideFrame Workflow Example](https://chipspeak.github.io/guideframe-action-example/) is triggered on push events and works to run the GuideFrame script on a GitHub runner before uploading the result as an artefact. The logic of the +workflow should be apparent. It once again serves as a simple starting point but it is advised to avoid manipulating the `Run Main Script With Display started` job in order to avoid compromising the render. \ No newline at end of file diff --git a/docs/guideframe-action-example.md b/docs/guideframe-action-example.md new file mode 100644 index 0000000..f47f855 --- /dev/null +++ b/docs/guideframe-action-example.md @@ -0,0 +1,49 @@ +--- +title: GuideFrame Workflow Example +layout: default +nav_order: 3 +parent: Samples +permalink: /guideframe-action-example/ +--- + +# GuideFrame Workflow Example + +```yaml +name: GuideFrame Video Render +# Trigger the workflow on push +on: [push] +# All jobs in the workflow +jobs: + magento-test: + runs-on: ubuntu-latest + # steps to run + steps: + # Checkout the code + - name: Checkout code + uses: actions/checkout@v2 + # Set up the environment required by GuideFrame + - name: Set up environment + run: | + sudo apt-get update + sudo apt-get install -y \ + ffmpeg \ + xvfb \ + chromium-driver \ + chromium-browser + pip install guideframe + # Run the main script (starting the virtual display first) + - name: Run Main Script with Display started + run: | + # Start Xvfb + export DISPLAY=:99 + nohup Xvfb :99 -screen 0 1920x1080x24 & + + # Run the Python script + python3 guideframe_demo.py github + # Upload the screen recording as an artifact + - name: Upload screen recording as artifact + uses: actions/upload-artifact@v4 + with: + name: guideframe-demo + path: ./guideframe_demo*.mp4 +``` \ No newline at end of file diff --git a/docs/guideframe-md-example.md b/docs/guideframe-md-example.md new file mode 100644 index 0000000..38fb0ad --- /dev/null +++ b/docs/guideframe-md-example.md @@ -0,0 +1,78 @@ +--- +title: GuideFrame Markdown Example +layout: default +nav_order: 2 +parent: Samples +permalink: /guideframe-md-example/ +--- + +# GuideFrame Markdown Example + +```Markdown +## Step 1 +This video serves as a demonstration of guideframe's use of selenium functions. We'll achieve this via the Magento testing site. + + +## Step 2 +We can use the click element function by passing in the elements I D. Let's use that to sign in. + + +## Step 3 +We can use the form fields here to demonstrate the type into fields function, which takes the element I D and the text you wish to pass as arguments. + + +## Step 4 +Now that we've filled the form, we can use the click element function again to sign in with these credentials. + + +## Step 5 +Next, we'll demonstrate the hover over element function by hovering over the gear dropdown in the navbar. This function takes an h ref as an argument. It is then used in an X path filter to locate the appropriate element. + + +## Step 6 +We'll once again use the click element function to select the fitness equipment link. + + +## Step 7 +Now we'll hover over the yoga companion kit using the same hovering function. + + +## Step 8 +Then we can move on to hover over the yoga straps. + + +## Step 9 +Then the strength band kit. + + +## Step 10 +Before returning to the yoga straps, this time using the hover and click function in order to first hover, then click on the link. + + +## Step 11 +Next, let's hover over the reviews section on the product page. + + +## Step 12 +We can use the open link in new tab function here to open the reviews section in a new tab and switch to it. + + +## Step 13 +Let's enter some text here using the same function for fields that we did earlier. + + +## Step 14 +Now we can click on the submit review button using the same functionality we've been using throughout. + + +## Step 15 +We can use the switch to tab function to return to the original tab by passing the tab index as an argument. + + +## Step 16 +Now that we're on the original tab, let's click the dropdown in the top right of the screen. We do this by using the click on button via span text function once again. + + +## Step 17 +Finally, let's click the sign out button to end this demonstration. +``` \ No newline at end of file diff --git a/docs/guideframe-py-example.md b/docs/guideframe-py-example.md new file mode 100644 index 0000000..413b081 --- /dev/null +++ b/docs/guideframe-py-example.md @@ -0,0 +1,192 @@ +--- +title: GuideFrame Script Example +layout: default +nav_order: 1 +parent: Samples +permalink: /guideframe-py-example/ +--- + +# GuideFrame Script Example + +```python +from guideframe.selenium import * # Importing all functions from selenium_functions.py +from guideframe.assembly import assemble # Importing the assemble_clips function from assembly.py +from guideframe.utils import get_env_settings, guide_step # Importing the guide_step and get_env_settings functions from guideframe_utils.py + + +# This function will run the full script +def guideframe_demo_script(): + try: + env_settings = get_env_settings() # Getting the environment settings + driver_location = env_settings["driver_location"] # Getting the driver location from the settings + driver = driver_setup(driver_location) # Initializing driver + set_window_size(driver) + open_url(driver, "https://magento.softwaretestingboard.com/") + + + ''' + Step 1 - Open the Magento website + ''' + guide_step( + 1, + lambda: None + ) + + + ''' + Step 2 - Click the "Sign In" link + ''' + guide_step( + 2, + lambda: click_element(driver, ".authorization-link > a") + ) + + + ''' + Step 3 - Enter email and password + ''' + guide_step( + 3, + lambda: type_into_field(driver, "email", "test-user@email.com"), + lambda: type_into_field(driver, "pass", "testuser-1") + ) + + + ''' + Step 4 - Click the "Sign In" button + ''' + guide_step( + 4, + lambda: click_element(driver, "button[name='send']") + ) + + + ''' + Step 5 - Hover over the "Gear" menu + ''' + guide_step( + 5, + lambda: hover_over_element(driver, "https://magento.softwaretestingboard.com/gear.html") + ) + + + ''' + Step 6 - Then, click on "Fitness Equipment" + ''' + guide_step( + 6, + lambda: click_element(driver, "a[href='https://magento.softwaretestingboard.com/gear/fitness-equipment.html']") + ) + + + ''' + Step 7 - Hover over the yoga companion kit + ''' + guide_step( + 7, + lambda: hover_over_element_by_xpath(driver, '//*[@id="maincontent"]/div[3]/div[1]/div[3]/ol/li[1]/div/a/span/span/img') + ) + + + ''' + Step 8 - Hover over the "Yoga Straps" product + ''' + guide_step( + 8, + lambda: hover_over_element(driver, "https://magento.softwaretestingboard.com/set-of-sprite-yoga-straps.html") + ) + + + ''' + Step 9 - Hover over the strength band kit + ''' + guide_step( + 9, + lambda: hover_over_element(driver, "https://magento.softwaretestingboard.com/harmony-lumaflex-trade-strength-band-kit.html") + ) + + + ''' + Step 10 - Return to the straps and click on them + ''' + guide_step( + 10, + lambda: hover_and_click(driver, "https://magento.softwaretestingboard.com/set-of-sprite-yoga-straps.html") + ) + + + ''' + Step 11 - Hover over the reviews link + ''' + guide_step( + 11, + lambda: hover_over_element(driver, "https://magento.softwaretestingboard.com/set-of-sprite-yoga-straps.html#review-form") + ) + + + ''' + Step 12 - Open the reviews link in a new tab + ''' + guide_step( + 12, + lambda: open_link_in_new_tab(driver, "https://magento.softwaretestingboard.com/set-of-sprite-yoga-straps.html#review-form") + ) + + + ''' + Step 13 - Enter the nickname, summary, and review + ''' + guide_step( + 13, + lambda: type_into_field(driver, "nickname_field", "Test User"), + lambda: type_into_field(driver, "summary_field", "Great product!"), + lambda: type_into_field(driver, "review_field", "I love this product!") + ) + + + ''' + Step 14 - Submit the review + ''' + guide_step( + 14, + lambda: click_button_by_span_text(driver, "Submit Review") + ) + + + ''' + Step 15 - Return to the first tab + ''' + guide_step( + 15, + lambda: switch_to_tab(driver, 0) + ) + + + ''' + Step 16 - Click the dropdown next to user name + ''' + guide_step( + 16, + lambda: click_button_by_span_text(driver, "Change") + ) + + + ''' + Step 17 - Click the "Log Out" button + ''' + guide_step( + 17, + lambda: click_element(driver, "a[href='https://magento.softwaretestingboard.com/customer/account/logout/']") + ) + + + finally: + print("Script complete -> moving to assembly") + driver.quit() + + +# Run the demo script and then use the assemble function +if __name__ == "__main__": + guideframe_demo_script() + assemble(17) +``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index ca0c545..f624a78 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,35 +1,17 @@ --- -title: Home +title: Overview layout: home +nav_order: 1 +permalink: /home/ --- -This is a *bare-minimum* template to create a Jekyll site that uses the [Just the Docs] theme. You can easily set the created site to be published on [GitHub Pages] – the [README] file explains how to do that, along with other details. +# GuideFrame -If [Jekyll] is installed on your computer, you can also build and preview the created site *locally*. This lets you test changes before committing them, and avoids waiting for GitHub Pages.[^1] And you will be able to deploy your local build to a different platform than GitHub Pages. +GuideFrame is a tool to allow software developers to produce detailed walkthrough videos of their projects using python code. It comes in the form of a python package which leverages numerous open source technologies. -More specifically, the created site: +At a high level, it allows the user to codify their video material, ensuring ease of reproduction. It aims to circumvent some of the presumed skills required of an engineer to produce engaging video content. +Its intended use is as a GitHub action. In this guise, GuideFrame can exist in a CI/CD pipeline where code changes can prompt a fresh render of a walkthrough video. -- uses a gem-based approach, i.e. uses a `Gemfile` and loads the `just-the-docs` gem -- uses the [GitHub Pages / Actions workflow] to build and publish the site on GitHub Pages +It allows users to define a script in plain markdown which will be consumed by GuideFrame's audio functions. This will in turn be applied to an interaction-based script which will carry out actions in a web UI and record the process. GuideFrame assembles all of these elements in order to render a complete video with recorded video interactions and matching voiceover. -Other than that, you're free to customize sites that you create with this template, however you like. You can easily change the versions of `just-the-docs` and Jekyll it uses, as well as adding further plugins. - -[Browse our documentation][Just the Docs] to learn more about how to use this theme. - -To get started with creating a site, simply: - -1. click "[use this template]" to create a GitHub repository -2. go to Settings > Pages > Build and deployment > Source, and select GitHub Actions - -If you want to maintain your docs in the `docs` directory of an existing project repo, see [Hosting your docs from an existing project repo](https://github.com/just-the-docs/just-the-docs-template/blob/main/README.md#hosting-your-docs-from-an-existing-project-repo) in the template README. - ----- - -[^1]: [It can take up to 10 minutes for changes to your site to publish after you push the changes to GitHub](https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/creating-a-github-pages-site-with-jekyll#creating-your-site). - -[Just the Docs]: https://just-the-docs.github.io/just-the-docs/ -[GitHub Pages]: https://docs.github.com/en/pages -[README]: https://github.com/just-the-docs/just-the-docs-template/blob/main/README.md -[Jekyll]: https://jekyllrb.com -[GitHub Pages / Actions workflow]: https://github.blog/changelog/2022-07-27-github-pages-custom-github-actions-workflows-beta/ -[use this template]: https://github.com/just-the-docs/just-the-docs-template/generate +The current iteration of GuideFrame is in Beta and is being submitted as a final project for the HDip in Computer Science in SETU. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..1f835c4 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,47 @@ +--- +title: Installation +layout: default +nav_order: 2 +permalink: /installation/ +--- + +# How to Install GuideFrame + +The following section will walk a user through the installation/setup of GuideFrame. This guide is written from 2 perspectives: + +1. A user who simply wishes to run GuideFrame on their GitHub repository. +2. A user who also wishes to run GuideFrame locally. + +## GitHub Installation + +GuideFrame is intended for use as part of a GitHub CI/CD pipeline. This involves featuring a GuideFrame script within your repository along with an appropriate GitHub action triggering it. + +In the interest of illustrating this, a template repository has been created and can be found [here]( +https://github.com/chipspeak/GuideFrame-Template). + +## Local Installation +GuideFrame is packaged and available on pypi. It can be installed using: + +```pip install guideframe``` + +Once installed, you will need to install the non-python dependencies. A setup script is packaged with GuideFrame for this. You can simply copy it from the GuideFrame repo and run it locally or you can run the following: + +```bash $(python -c "import guideframe, os; print(os.path.join(os.path.dirname(guideframe.__file__), 'setup_env.sh'))")``` + +Alternatively, depending on you operating system, you can run the following to install the package and dependencies in one: + +```bash + sudo apt-get update + sudo apt-get install -y \ + ffmpeg \ + xvfb \ + chromium-driver \ + chromium-browser + pip install guideframe +``` + +You will then need to create a GuideFrame script. See the repository link above for a template. Once this script has been created, you can run it using: + +```bash python ``` + +Note: GuideFrame currently supports ```macos``` as its system argument. You can also pass: ```github``` to run on an ubuntu system but this has not been tested outside of virtual machines. \ No newline at end of file diff --git a/docs/library.md b/docs/library.md new file mode 100644 index 0000000..6212658 --- /dev/null +++ b/docs/library.md @@ -0,0 +1,9 @@ +--- +title: Library +layout: default +nav_order: 4 +permalink: /library/ +--- + +# Understanding GuideFrame +The GuideFrame package and its core library of functions will be broken down in the following pages. Viewing these pages is highly advised in order to grasp the underlying logic and syntax of GuideFrame. \ No newline at end of file diff --git a/docs/samples.md b/docs/samples.md new file mode 100644 index 0000000..ccfa26d --- /dev/null +++ b/docs/samples.md @@ -0,0 +1,10 @@ +--- +title: Samples +layout: default +nav_order: 5 +permalink: /samples/ +--- + +# Samples + +This section contains two example files which serve to demonstrate GuideFrame script and markdown construction. These examples also feature in the [GuideFrame Template Repository](https://github.com/chipspeak/GuideFrame-Template). \ No newline at end of file diff --git a/docs/selenium.md b/docs/selenium.md new file mode 100644 index 0000000..e70154d --- /dev/null +++ b/docs/selenium.md @@ -0,0 +1,280 @@ +--- +title: Selenium +layout: default +nav_order: 4 +parent: Library +permalink: /selenium/ +--- + +# Selenium +The selenium file contains functions designed to perform the various UI-based interactions specified in a GuideFrame step. The functions act as an SDK-lite, providing an abstraction layer to users who wish to avoid more escoteric `selenium` commands. The following section will list each function contained within this file and provide some insight into its use and syntax. + + +`driver_setup()` +```python +def driver_setup(driver_location): + # Setting up with Chrome options and the ChromeDriver service + options = Options() + options.add_argument("usr/bin/google-chrome") + options.add_argument("--incognito") + + # Disable the "Chrome is being controlled by automated test software" banner + options.add_experimental_option("excludeSwitches", ["enable-automation"]) + options.add_experimental_option("useAutomationExtension", False) + + # Specify the path to ChromeDriver + service = Service(driver_location) + + # Initialize the WebDriver + driver = webdriver.Chrome(service=service, options=options) + + return driver +``` +This function takes the `driver_location` variable extracted by `get_env_settings()` in utils. The function then adds numerous `selenium` options in to provide the optimum setup for GuideFrame. This includes using incognito mode to avoid password saving prompts and disabling the chrome banner stating the use of automation in the session. Once the various options have been set, the function returns the `driver` which will be used as an argument in all of the below functions. + + +### `open_url()` +```python +def open_url(driver, target): + driver.get(target) +``` +This function simply takes the `driver` and a url as arguments. It then opens the passed url in the browser. + + +### `set_window_size()` +```python +def set_window_size(driver): + # Try block to account for potential driver issues + try: + driver.maximize_window() + # If an exception is caught, manually set the window size + except Exception as e: + print("Error maximizing window:", e) + print("Setting window size to 1920x1080") + driver.set_window_size(1920, 1080) +``` +This function uses the `driver` as an argument and then uses the requisite `selenium` command to maximise the browser window, ensuring a full screen representation of the session. It includes a try block to account for potential errors due to `chromedriver` updates. Should the initial command fail, it will fall back to using a 1920x1080 pixel count. + + +### `find_element()` +```python +def find_element(driver, id): + try: + element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, id)) + ) + return element + except Exception as e: + print(f"Error finding element with ID '{id}': {e}") + raise +``` +This function takes the `driver` and an elements `id` as arguments. It then uses `selenium` functions to wait for the element to appear. This is wrapped in a try block ensuring that if an element is not found with a matching `id`, then an exception is raised. + + +### `scroll_to_element()` +```python +def scroll_to_element(driver, href): + try: + # Use WebDriverWait to ensure the element is present + element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.XPATH, f"//a[@href='{href}']")) + ) + driver.execute_script("arguments[0].scrollIntoView()", element) + + except Exception as e: + print(f"Error in scroll_to_element for href '{href}': {e}") + raise +``` +This function takes the `driver` and a `href` as arguments. It once again uses `selenium` functions to wait for the presence of an element. In this case however, an xpath filter is used to find the element by its `href`. Once this has occured, the `selenium` functions to scroll to an element are invoked with the result of the xpath check passed. + + +### `hover_and_click()` +```python +def hover_and_click(driver, href): + try: + # Use WebDriverWait to ensure the element is present + element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.XPATH, f"//a[@href='{href}']")) + ) + # Use ActionChains to hover over the element + actions = ActionChains(driver) + actions.move_to_element(element).perform() + + # Click the element + element.click() + except Exception as e: + print(f"Error in hover_and_click for href '{href}': {e}") + raise +``` +This function once again takes a `driver` and `href` as arguments. It uses the same logic as the previous function to find the element by the `href` but in this case, `selenium` is invoked to perform the `move_to_element` interaction. Once this has occured, the element is clicked. + + +### `hover_over_element()` +```python +def hover_over_element(driver, href): + try: + # Use WebDriverWait to ensure the element is present + element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.XPATH, f"//a[@href='{href}']")) + ) + actions = ActionChains(driver) + actions.move_to_element(element).perform() + + except Exception as e: + print(f"Error in hover_over_element for href '{href}': {e}") + raise +``` +This function is identical to the previous one with the exception that it doesn't click the element. Useful for highlight a linked button etc without following through on the click. + +### `click_element()` +```python +def click_element(driver, css_selector): + try: + element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CSS_SELECTOR, css_selector)) + ) + element.click() + except Exception as e: + print(f"Error clicking element with selector '{css_selector}': {e}") + raise +``` +This function uses similar logic to the `find_element()` function with the exception of adding a click to the sequence and using a `css_selector` rather than an `id` to locate the element. This is useful for situations where an `id` may not be static. + + +### `type_into_field()` +```python +def type_into_field(driver, element_id, text): + try: + input_field = WebDriverWait(driver, 10).until( + EC.visibility_of_element_located((By.ID, element_id)) + ) + input_field.send_keys(text) + except Exception as e: + print(f"Error typing into field with ID '{element_id}': {e}") + raise +``` +This function uses the same element-locating logic seen throughout this file with the addition of a call to the `selenium` function, `send_keys` where the `text` argument from this function is passed. + + +### `open_link_in_new_tab()` +```python +def open_link_in_new_tab(driver, href): + try: + # Open the link in a new tab + driver.execute_script(f"window.open('{href}', '_blank');") + + # Switch to the newly opened tab + driver.switch_to.window(driver.window_handles[-1]) + except Exception as e: + print(f"Error opening link '{href}' in a new tab: {e}") + raise +``` +This function takes the `driver` and a `href` as an argument. It uses the `execute_script()` function within selenium to pass script arguments. In this case a window is opened using the passed `href`. The `switch_to.window()` function from `selenium` is then called where it takes the most recently opened tab as an argument. This is found using the size of the `window_handles` array and subtracting 1 to find the most recently opened window. + + +### `switch_to_tab()` +```python +def switch_to_tab(driver, tab_index): + try: + if 0 <= tab_index < len(driver.window_handles): + driver.switch_to.window(driver.window_handles[tab_index]) + else: + print(f"Invalid tab index: {tab_index}") + except Exception as e: + print(f"Error switching to tab {tab_index}: {e}") + raise +``` +This function takes the `driver` and `tab_index` as arguments. The user simply needs to specify which index of the array of open tabs they wish to switch to. This is wrapped in conditional logic to ensure an invalid index isn't provided. The `window_handles()` function is then called with `tab_index` passed in order to open the correct tab. + + +### `take_screenshot()` +```python +def take_screenshot(driver, file_name="screenshot.png"): + try: + driver.save_screenshot(file_name) + except Exception as e: + print(f"Error taking screenshot: {e}") + raise +``` +This function takes the `driver` and a `file_name` as arguments. It has a default of "screenshot.png". It uses `selenium` to take a screenshot and use the argument to name the file. + + +### `select_dropdown_option()` +``` python +def select_dropdown_option(driver, dropdown_id, visible_text): + try: + dropdown = Select(WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.ID, dropdown_id)) + )) + dropdown.select_by_visible_text(visible_text) + print(f"Selected dropdown option: {visible_text}") + except Exception as e: + print(f"Error selecting dropdown option '{visible_text}': {e}") + raise +``` +This function takes the `driver`, `dropdown_id` and `visible_text` as arguments. It uses `selenium` logic to ensure that the element is clickable before using the `select_by_visible_text()` function to click on a dropdown option with text matching the functions argument. + + +### `click_button_by_span_text()` +```python +def click_button_by_span_text(driver, span_text): + try: + # XPath to find a button containing a span with the given text + button = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.XPATH, f"//button[span[text()='{span_text}']]")) + ) + button.click() + print(f"Clicked button with span text: '{span_text}'") + except Exception as e: + print(f"Error clicking button with span text '{span_text}': {e}") + raise +``` +This function uses similar logic to other clicking functions but in this case uses an xpath filter to locate an element by the `span_text` argument passed to the function. This is useful for buttons in particular or other elements with static span text. + + +### `click_element_by_xpath()` +```python +def click_element_by_xpath(driver, xpath): + try: + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + element.click() + except Exception as e: + print(f"Error clicking element with xpath '{xpath}': {e}") + raise +``` +This function is similar to other outlined throughout this document except that it takes an `xpath` as an argument. This allows a user to simply use a browser's `inspect` feature to select an element, right click and then select `copy xpath`. This can then be passed to this function. + + +### `hover_over_element_by_xpath()` +```python +def hover_over_element_by_xpath(driver, xpath): + try: + element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.XPATH, xpath)) + ) + actions = ActionChains(driver) + actions.move_to_element(element).perform() + except Exception as e: + print(f"Error hovering over element with xpath '{xpath}': {e}") + raise +``` +This function, similar to the above example, is similar to the other hovering functions with the exception of using `xpath` to locate elements. As above, this streamlines the user experience in terms of locating elements in the browser prior to GuideFrame script creation. + + +### `highlight_github_code()` +```python +def highlight_github_code(driver, target): + driver.get(target) + driver.refresh() +``` +This function matchess the `open_url()` function but refreshes the page once it's opened. This occurs to allow the user to pass GitHub urls with line numbers for code walkthroughs. By default, when on a GitHub page, adding the line numbers to the url will not move to the requisite line. A refresh is required, hence this implementation. + + +### `sleep_for()` +```python +def sleep_for(seconds): + sleep(seconds) +``` +This function exists to keep all GuideFrame interaction functionality contained within one file. It uses `time.sleep()` to take the `seconds` argument and sleep for the duration. This function can be called using `lambda` in a `guide_step()` call. This allows a user to inject customized waits between GuideFrame actions. diff --git a/docs/utils.md b/docs/utils.md new file mode 100644 index 0000000..df0f643 --- /dev/null +++ b/docs/utils.md @@ -0,0 +1,101 @@ +--- +title: Utils +layout: default +nav_order: 1 +parent: Library +permalink: /utils/ +--- + +# Utils +The utils file contains functions designed to provide vital variables to other aspects of the GuideFrame logic in addition to outlining the logic of the key `guide_step`. The following section will list each function contained within this file and provide some insight into its use and syntax. + + +### `get_env_settings()` +```python +def get_env_settings(): + if len(sys.argv) > 1: + env = sys.argv[1] # Getting the environment argument + else: + print("No environment argument provided. Use 'macos' or 'github'.") + sys.exit(1) + + # Define settings based on environment + if env == "macos": + return { + "input_format": "avfoundation", + "input_display": "1", + "driver_location": "/opt/homebrew/bin/chromedriver" + } + elif env == "github": + return { + "input_format": "x11grab", + "input_display": ":99.0", + "driver_location": "/usr/bin/chromedriver" + } + else: + print("Invalid environment specified. Use 'macos' or 'github'.") + sys.exit(1) +``` +This function takes the system argument provided to the GuideFrame script and sets vital environmental variables based on this. This function is key in accounting for the variance in file paths, display type etc. + + +### `extract_md_filename()` +```python +def extract_md_filename(): + script_name = sys.argv[0] + return script_name.replace(".py", ".md") +``` +This function extracts the GuideFrame scripts name from the system argument before replacing the `.py` extension with `.md`. This is performed in order to ascertain the title of the GuideFrame scripts matching markdown file. The markdown file MUST match the GuideFrame scripts title or the core logic will fail. + + +### `extract_script_name()` +```python +def extract_script_name(): + script_name = sys.argv[0] + return script_name.replace(".py", "") +``` +This function serves a similar purpose and shares logic with `extract_md_filename()`. It is used to drop the `.py` extension in order to grab the scripts name for final output file naming. + + +### ```guide_step()``` +```python +def guide_step(step_number, *actions, order="action-after-vo"): + # Get the environment settings + env_settings = get_env_settings() + input_format = env_settings["input_format"] + input_display = env_settings["input_display"] + md_file = extract_md_filename() + + # Start the recording for the step + step = start_ffmpeg_recording(f"step{step_number}.mp4", input_format, input_display) + + # Conditional logic to account for vo relative to action + if order == "action-before-vo": + for action in actions: + action() + generate_voicover(md_file, step_number) + else: # Default order is action-after-vo + generate_voicover(md_file, step_number) + for action in actions: + action() + + stop_ffmpeg_recording(step) +``` +This is the function which carries out each individual step of a GuideFrame script. It takes multiple arguments in the form of: +* `step_number` in order to match to the correct markdown step number. +* `*actions` to allows the user to pass any number of functions via `lambda`. +* `order` which defaults to `"actions-after-vo"`. This allows the user some level of control over how actions and voiceover are recorded relative to each other. + +Its logic follows the below order: +1. It calls `get_env_settings()` in order to extract the variables it will need to pass to `ffmpeg`. +2. It then calls `extract_md_filename()` in order to later pass this filename to `generate_voiceover()`. +3. It then calls `start_ffmpeg_recording()`, passing the step number, in order to name the output file accordingly, alongside the aforementioned input variables. +4. It checks the status of the `order` variable before iterating through the actions before or after calling `generate_voiceover`. +5. Once the above has been completed, it calls `stope_ffmpeg_recording` and the step's video and matching audio are now complete as separate files. + + + + + + + diff --git a/docs/video.md b/docs/video.md new file mode 100644 index 0000000..f85eeb7 --- /dev/null +++ b/docs/video.md @@ -0,0 +1,46 @@ +--- +title: Video +layout: default +nav_order: 2 +parent: Library +permalink: /video/ +--- + +# Video +The video file contains functions designed to provide start and stop the `ffmpeg` recordings used to capture each GuideFrame step. The following section will list each function contained within this file and provide some insight into its use and syntax. + + +### `start_ffmpeg_recording()` +```python +def start_ffmpeg_recording(output_file, input_format, input_display): + print("Beginning recording of clip") + command = [ + 'ffmpeg', + '-f', input_format, # Input format + '-video_size', '1920x1080', # Resolution + '-framerate', '30', # Frame rate + '-i', input_display, # Input display (1 or :99.0 for GitHub actions) + '-vcodec', 'libxvid', # Video codec + '-preset', 'fast', # Preset for encoding speed + '-b:v', '3000k', # Bitrate + '-pix_fmt', 'yuv420p', # Pixel format + output_file # Output file path + ] + process = subprocess.Popen(command, stdin=subprocess.PIPE) + return process +``` +This function is responsible for starting the `ffmpeg` recording which will be used to capture the virtual screen on which the GuideFrame interactions are occuring. + +It takes the `output_file`, `input_format` and `input_display` variables in order to account for the environment differences in command flags and the desired final file name. + +It then uses `subprocess` to run the `ffmpeg` command, passing the array of flags outlined above. + + +### `stop_ffmpeg_recording()` +```python +def stop_ffmpeg_recording(process): + process.stdin.write(b"q\n") # Send 'q' to gracefully stop the recording + process.communicate() # Wait for the process to finish + print("Ending recording of clip") +``` +This function takes the return value of the previous function and passes `q` to standard in. This prompts the running process to quit, completing the recording cycle. \ No newline at end of file