with_clues 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e8a1a2ede55c2de31c9a7a7e53a576b4a52dd08001b87a9406e8b9a16fa5495
4
- data.tar.gz: 7e75e12ebe1d192c74813521f4800d2e70dd2641e1c8e70f5cda563fea700100
3
+ metadata.gz: b17d4876d0162d52c3cf3e767c70b26cb2d18ff9ed144eda164ddf3afdbcd6f7
4
+ data.tar.gz: 6e605bc07cae10efaf1b3866987f31cc36bd4bf619b5414b8d43d0f8c7b0c76a
5
5
  SHA512:
6
- metadata.gz: 2c3722af382010534c4020a70b17ff7c6907c101fc0f6159758193a8152b30850a0b208dcfd6a94d1128275b8026c524bad9a2e4cac55bd292cb4c4bc9dc0645
7
- data.tar.gz: c90b2558520118f6617db83b2a3a48317ee51a03bfd01fc3eec9a726f6e4b5aea34fccc03cae5d74c03aed117eefdf8b39942084f59eed2ecadcb3784954907a
6
+ metadata.gz: 104eb1b602cd79c0dd3183daf5e47dff90e39bbeed82c06209b079902bc7e27d5a49631a22bc76c2a801a9193d3f8d8e0f026755962a2688fa135fb73aa9b426
7
+ data.tar.gz: 9c8bf899641fe07713725bd7ed7591ff85e59ad2e61c05b508ecddfc71f3a9c352132ed685e0f0cb3d16b304403c18f039626fa0a5e153d51002161ab66edab4
data/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ See [https://github.com/sustainable-rails/with\_clues/releases](https://github.com/sustainable-rails/with_clues/releases)
data/Dockerfile.dx ADDED
@@ -0,0 +1,28 @@
1
+ ARG RUBY_VERSION
2
+ FROM ruby:${RUBY_VERSION}
3
+
4
+ ENV DEBIAN_FRONTEND noninteractive
5
+ RUN apt-get -y update
6
+
7
+
8
+ # dx.snippet.start=templates/snippets/bundler/latest__bundler.dockerfile-snippet
9
+ # Based on documentation at https://guides.rubygems.org/command-reference/#gem-update
10
+ # based on the vendor's documentation
11
+ RUN echo "gem: --no-document" >> ~/.gemrc && \
12
+ gem update --system && \
13
+ gem install bundler
14
+
15
+ # dx.snippet.end=templates/snippets/bundler/latest__bundler.dockerfile-snippet
16
+
17
+
18
+ # dx.snippet.start=templates/snippets/vim/bullseye_vim.dockerfile-snippet
19
+ # Based on documentation at https://packages.debian.org/search?keywords=vim
20
+ # based on the vendor's documentation
21
+ ENV EDITOR=vim
22
+ RUN apt-get install -y vim && \
23
+ echo "set -o vi" >> /root/.bashrc
24
+ # dx.snippet.end=templates/snippets/vim/bullseye_vim.dockerfile-snippet
25
+
26
+ # This entrypoint produces a nice help message and waits around for you to do
27
+ # something with the container.
28
+ COPY dx/show-help-in-app-container-then-wait.sh /root
data/README.md CHANGED
@@ -6,8 +6,7 @@ Suppose you have this:
6
6
  expect(page).to have_content("My Awesome Site")
7
7
  ```
8
8
 
9
- And Capybara says that that content is not there and that is all it says. You might slap in a `puts page.html` and try again.
10
- Instead, what if you could not do that and do this?
9
+ And Capybara says that that content is not there and that is all it says. You might slap in a `puts page.html` and try again. Instead, what if you could not do that and do this?
11
10
 
12
11
  ```ruby
13
12
  with_clues do
@@ -71,8 +70,7 @@ end
71
70
 
72
71
  ## Use
73
72
 
74
- In general, you would not want to wrap all tests with `with_clues`. This is a diagnostic tool to allow you to get more
75
- information on a test that is failing. As such, your workflow might be:
73
+ In general, you would not want to wrap all tests with `with_clues`. This is a diagnostic tool to allow you to get more information on a test that is failing. As such, your workflow might be:
76
74
 
77
75
  1. Notice a test failing that you cannot easily diagnose
78
76
  1. Wrap the failing assertion in `with_clues`:
@@ -89,8 +87,10 @@ information on a test that is failing. As such, your workflow might be:
89
87
 
90
88
  There are three clues included:
91
89
 
92
- * Dumping HTML - when `page` exists, it will dump the contents of `page.html` when the test fails
93
- * Dumping Browser logs - for a browser-based test, it will dump anything that was `console.log`'ed
90
+ * Dumping HTML - when `page` exists, it will dump the contents of `page.html` (for Selenium) or `page.content`
91
+ (for Playwright) when the test fails
92
+ * Dumping Browser logs - for a browser-based test, it will dump anything that was `console.log`'ed. This should
93
+ work with Selenium and Playwright
94
94
  * Arbitrary context you pass in, for example when testing an Active Record
95
95
 
96
96
  ```ruby
@@ -107,13 +107,16 @@ There are three clues included:
107
107
  `with_clues` is intended as a diagnostic tool you can develop and enhance over time. As your team writes more code or develops
108
108
  more conventions, you can develop diagnostics as well.
109
109
 
110
- To add one, create a class that implements `dump(notifier, context:)`:
110
+ To add one, create a class that implements `dump(notifier, context:)` or `dump(notifier, context:, page:)` or
111
+ `dump(notifier, context:, page:, captured_logs)`:
111
112
 
112
- * `notifier` is a `WithClues::Notifier` that you should use to produce output:
113
- * `notify` - output text, preceded with `[ with_clues ]` (this is so you can tell output from your code vs from `with_clues`)
114
- * `blank_line` - a blank line (no prefix)
115
- * `notify_raw` - output text without a prefix, useful for removing ambiguity about what is being output
113
+ * `notifier` is a `WithClues::Notifier` that you should use to produce output via the following methods:
114
+ * `notifier.notify` - output text, preceded with `[ with_clues ]` (this is so you can tell output from your code vs from `with_clues`)
115
+ * `notifier.blank_line` - a blank line (no prefix)
116
+ * `notifier.notify_raw` - output text without a prefix, useful for removing ambiguity about what is being output
116
117
  * `context:` the context passed into `with_clues` (nil if it was omitted)
118
+ * `page:` will be given the Selenium or Playwright page object
119
+ * `captured_logs:` for Playwright, this will be the browser console logs captured inside the block
117
120
 
118
121
  For example, suppose you want to output information about an Active Record like so:
119
122
 
@@ -153,6 +156,9 @@ WithClues::Method.use_custom_clue ActiveRecordClues
153
156
 
154
157
  You can use multiple clues by repeatedly calling `use_custom_clue`
155
158
 
159
+ Note that if your clue implements the three-arg version of `dump` ( `dump(notifier, context:, page:)` ), it will *only* be used when in
160
+ a context where Capybara's `page` element in in play.
161
+
156
162
  ## Developing
157
163
 
158
164
  * Get set up with `bin/setup`
data/bin/matrix ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+
3
+ . dx/docker-compose.env
4
+ for version in ${RUBY_VERSIONS[@]}; do
5
+ dx/exec -v $version bin/setup && dx/exec -v $version bin/ci
6
+ done
@@ -0,0 +1,33 @@
1
+ # THIS IS GENERATED - DO NOT EDIT
2
+
3
+ services:
4
+ with-clues-3.1:
5
+ image: sustainable-rails/with-clues-dev:ruby-3.1
6
+ init: true
7
+ volumes:
8
+ - type: bind
9
+ source: "./"
10
+ target: "/root/work"
11
+ consistency: "consistent"
12
+ entrypoint: /root/show-help-in-app-container-then-wait.sh
13
+ working_dir: /root/work
14
+ with-clues-3.2:
15
+ image: sustainable-rails/with-clues-dev:ruby-3.2
16
+ init: true
17
+ volumes:
18
+ - type: bind
19
+ source: "./"
20
+ target: "/root/work"
21
+ consistency: "consistent"
22
+ entrypoint: /root/show-help-in-app-container-then-wait.sh
23
+ working_dir: /root/work
24
+ with-clues-3.3:
25
+ image: sustainable-rails/with-clues-dev:ruby-3.3
26
+ init: true
27
+ volumes:
28
+ - type: bind
29
+ source: "./"
30
+ target: "/root/work"
31
+ consistency: "consistent"
32
+ entrypoint: /root/show-help-in-app-container-then-wait.sh
33
+ working_dir: /root/work
data/dx/build ADDED
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
6
+
7
+ . "${SCRIPT_DIR}/dx.sh.lib"
8
+
9
+ require_command "docker"
10
+ load_docker_compose_env
11
+
12
+ usage_on_help "Builds the Docker image based on the Dockerfile" "" "build.pre" "build.post" "${@}"
13
+
14
+ for ruby_version in ${RUBY_VERSIONS[@]}; do
15
+ dockerfile="Dockerfile.dx"
16
+ docker_image_name="${IMAGE}:ruby-${ruby_version}"
17
+
18
+ log "Building for Ruby '${ruby_version}' using Docker image name '${docker_image_name}'"
19
+
20
+ exec_hook_if_exists "build.pre" "${dockerfile}" "${docker_image_name}"
21
+
22
+ docker build \
23
+ --file "${dockerfile}" \
24
+ --build-arg="RUBY_VERSION=${ruby_version}" \
25
+ --tag "${docker_image_name}" \
26
+ ./
27
+
28
+ exec_hook_if_exists "build.post" "${dockerfile}" "${docker_image_name}"
29
+ log "🌈" "Your Docker image has been built tagged '${docker_image_name}'"
30
+ done
31
+
32
+ log "✅" "All images built"
33
+
34
+ log "✨" "Creating docker-compose.dx.yml"
35
+ compose_file="docker-compose.dx.yml"
36
+ log "🗑️" "Deleting previous ${compose_file}"
37
+
38
+ rm -f "${compose_file}"
39
+ echo "# THIS IS GENERATED - DO NOT EDIT" > "${compose_file}"
40
+ echo "" >> "${compose_file}"
41
+ echo "services:" >> "${compose_file}"
42
+
43
+ for ruby_version in ${RUBY_VERSIONS[@]}; do
44
+ log "Generating stanza for version '${ruby_version}'"
45
+ docker_image_name="${IMAGE}:ruby-${ruby_version}"
46
+ echo " with-clues-${ruby_version}:" >> "${compose_file}"
47
+ echo " image: ${docker_image_name}" >> "${compose_file}"
48
+ echo " init: true" >> "${compose_file}"
49
+ echo " volumes:" >> "${compose_file}"
50
+ echo " - type: bind" >> "${compose_file}"
51
+ echo " source: \"./\"" >> "${compose_file}"
52
+ echo " target: \"/root/work\"" >> "${compose_file}"
53
+ echo " consistency: \"consistent\"" >> "${compose_file}"
54
+ echo " entrypoint: /root/show-help-in-app-container-then-wait.sh" >> "${compose_file}"
55
+ echo " working_dir: /root/work" >> "${compose_file}"
56
+ done
57
+ log "🎼" "${compose_file} is now created"
58
+ log "🔄" "You can run dx/start to start it up, though you may need to stop it first with Ctrl-C"
59
+
60
+ # vim: ft=bash
@@ -0,0 +1,5 @@
1
+ # This array must include the oldest Ruby first!
2
+ RUBY_VERSIONS=("3.1" "3.2" "3.3")
3
+ IMAGE=sustainable-rails/with-clues-dev
4
+ PROJECT_NAME=with-clues
5
+ # vim: ft=bash
data/dx/dx.sh.lib ADDED
@@ -0,0 +1,24 @@
1
+ # shellcheck shell=bash
2
+
3
+ . "${SCRIPT_DIR}/setupkit.sh.lib"
4
+
5
+ require_command "realpath"
6
+ require_command "cat"
7
+
8
+ ENV_FILE=$(realpath "${SCRIPT_DIR}")/docker-compose.env
9
+
10
+ load_docker_compose_env() {
11
+ . "${ENV_FILE}"
12
+ }
13
+
14
+ exec_hook_if_exists() {
15
+ script_name=$1
16
+ shift
17
+ if [ -x "${SCRIPT_DIR}"/"${script_name}" ]; then
18
+ log "🪝" "${script_name} exists - executing"
19
+ "${SCRIPT_DIR}"/"${script_name}" "${@}"
20
+ else
21
+ debug "${script_name} does not exist"
22
+ fi
23
+ }
24
+ # vim: ft=bash
data/dx/exec ADDED
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
6
+
7
+ . "${SCRIPT_DIR}/dx.sh.lib"
8
+
9
+ require_command "docker"
10
+ load_docker_compose_env
11
+
12
+ usage_description="Execute a command inside the app's container."
13
+ usage_args="[-s service] [-v ruby_version] command"
14
+ usage_pre="exec.pre"
15
+ usage_on_help "${usage_description}" "${usage_args}" "${usage_pre}" "" "${@}"
16
+
17
+ LATEST_RUBY=${RUBY_VERSIONS[0]}
18
+ DEFAULT_SERVICE=with-clues-${LATEST_RUBY}
19
+ SERVICE="${SERVICE_NAME:-${DEFAULT_SERVICE}}"
20
+ while getopts "v:s:" opt "${@}"; do
21
+ case ${opt} in
22
+ v )
23
+ SERVICE="with-clues-${OPTARG}"
24
+ ;;
25
+ s )
26
+ SERVICE="${OPTARG}"
27
+ ;;
28
+ \? )
29
+ log "🛑" "Unknown option: ${opt}"
30
+ usage "${description}" "${usage_args}" "${usage_pre}"
31
+ ;;
32
+ : )
33
+ log "🛑" "Invalid option: ${opt} requires an argument"
34
+ usage "${description}" "${usage_args}" "${usage_pre}"
35
+ ;;
36
+ esac
37
+ done
38
+ shift $((OPTIND -1))
39
+
40
+ if [ $# -eq 0 ]; then
41
+ log "🛑" "You must provide a command e.g. bash or ls -l"
42
+ usage "${description}" "${usage_args}" "${usage_pre}"
43
+ fi
44
+
45
+
46
+ exec_hook_if_exists "exec.pre"
47
+
48
+ log "🚂" "Running '${*}' inside container with service name '${SERVICE}'"
49
+
50
+ docker \
51
+ compose \
52
+ --file docker-compose.dx.yaml \
53
+ --project-name "${PROJECT_NAME}" \
54
+ --env-file "${ENV_FILE}" \
55
+ exec \
56
+ "${SERVICE}" \
57
+ "${@}"
58
+
59
+ # vim: ft=bash
data/dx/prune ADDED
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
6
+
7
+ . "${SCRIPT_DIR}/dx.sh.lib"
8
+ require_command "docker"
9
+ load_docker_compose_env
10
+
11
+ usage_on_help "Prune containers for this repo" "" "" "" "${@}"
12
+
13
+ for container_id in $(docker container ls -a -f "name=^${PROJECT_NAME}-.*-1$" --format="{{.ID}}"); do
14
+ log "🗑" "Removing container with id '${container_id}'"
15
+ docker container rm "${container_id}"
16
+ done
17
+ echo "🧼" "Containers removed"
18
+
19
+ # vim: ft=bash
@@ -0,0 +1,144 @@
1
+ # shellcheck shell=bash
2
+
3
+ fatal() {
4
+ remainder=${*:2}
5
+ if [ -z "$remainder" ]; then
6
+ log "🛑" "${@}"
7
+ else
8
+ log "${@}"
9
+ fi
10
+ exit 1
11
+ }
12
+
13
+ log() {
14
+ emoji=$1
15
+ remainder=${*:2}
16
+ if [ -z "${NO_EMOJI}" ]; then
17
+ echo "[ ${0} ] ${*}"
18
+ else
19
+ # if remainder is empty that means no emoji was passed
20
+ if [ -z "$remainder" ]; then
21
+ echo "[ ${0} ] ${*}"
22
+ else # emoji was passed, but we ignore it
23
+ echo "[ ${0} ] ${remainder}"
24
+ fi
25
+ fi
26
+ }
27
+
28
+ debug() {
29
+ message=$1
30
+ if [ -z "${DOCKBOX_DEBUG}" ]; then
31
+ return
32
+ fi
33
+ log "🐛" "${message}"
34
+ }
35
+
36
+ usage() {
37
+ description=$1
38
+ arg_names=$2
39
+ pre_hook=$3
40
+ post_hook=$4
41
+ echo "usage: ${0} [-h] ${arg_names}"
42
+ if [ -n "${description}" ]; then
43
+ echo
44
+ echo "DESCRIPTION"
45
+ echo " ${description}"
46
+ fi
47
+ if [ -n "${pre_hook}" ] || [ -n "${post_hook}" ]; then
48
+ echo
49
+ echo "HOOKS"
50
+ if [ -n "${pre_hook}" ]; then
51
+ echo " ${pre_hook} - if present, called before the main action"
52
+ fi
53
+ if [ -n "${post_hook}" ]; then
54
+ echo " ${post_hook} - if present, called after the main action"
55
+ fi
56
+ fi
57
+ exit 0
58
+ }
59
+
60
+ usage_on_help() {
61
+ description=$1
62
+ arg_names=$2
63
+ pre_hook=$3
64
+ post_hook=$4
65
+ # These are the args passed to the invocation so this
66
+ # function can determine if the user requested help
67
+ cli_args=( "${@:5}" )
68
+
69
+ for arg in "${cli_args[@]}"; do
70
+ if [ "${arg}" = "-h" ] || [ "${arg}" = "--help" ]; then
71
+ usage "${description}" "${arg_names}" "${pre_hook}" "${post_hook}"
72
+ fi
73
+ done
74
+ }
75
+
76
+ # Read user input into the variable 'INPUT'
77
+ #
78
+ # Args:
79
+ #
80
+ # [1] - an emoji to use for messages
81
+ # [2] - the message explaining what input is being requested
82
+ # [3] - a default value to use if no value is provided
83
+ #
84
+ # Respects NO_EMOJI when outputing messages to the user
85
+ user_input() {
86
+ emoji=$1
87
+ message=$2
88
+ default=$3
89
+ prompt=$4
90
+
91
+ if [ -z "$message" ]; then
92
+ echo "user_input requires a message"
93
+ exit 1
94
+ fi
95
+
96
+ INPUT=
97
+
98
+ if [ -z "${prompt}" ]; then
99
+ prompt=$(log "${emoji}" "Value: ")
100
+ if [ -n "${default}" ]; then
101
+ prompt=$(log "${emoji}" "Value (or hit return to use '${default}'): ")
102
+ fi
103
+ fi
104
+
105
+ while [ -z "${INPUT}" ]; do
106
+
107
+ log "$emoji" "$message"
108
+ read -r -p "${prompt}" INPUT
109
+ if [ -z "$INPUT" ]; then
110
+ INPUT=$default
111
+ fi
112
+ if [ -z "$INPUT" ]; then
113
+ log "😶", "You must provide a value"
114
+ fi
115
+ done
116
+ }
117
+
118
+ user_confirm() {
119
+ user_input "$1" "$2" "$3" "y/n> "
120
+ }
121
+
122
+ require_not_exist() {
123
+ file=$1
124
+ message=$2
125
+ if [ -e "${file}" ]; then
126
+ fatal "$message"
127
+ fi
128
+ }
129
+ require_exist() {
130
+ file=$1
131
+ message=$2
132
+ if [ ! -e "${file}" ]; then
133
+ fatal "$message"
134
+ fi
135
+ }
136
+
137
+ require_command() {
138
+ command_name=$1
139
+ if ! command -v "${command_name}" >/dev/null 2>&1; then
140
+ fatal "Command '${command_name}' not found - it is required for this script to run"
141
+ fi
142
+ }
143
+
144
+ # vim: ft=bash
@@ -0,0 +1,38 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ # Ideally, the message below is shown after everything starts up. We can't
6
+ # achieve this using healtchecks because the interval for a healtcheck is
7
+ # also an initial delay, and we don't really want to do healthchecks on
8
+ # our DB or Redis every 2 seconds. So, we sleep just a bit to let
9
+ # the other containers start up and vomit out their output first.
10
+ sleep 2
11
+ # Output some helpful messaging when invoking `dx/start` (which itself is
12
+ # a convenience script for `docker compose up`.
13
+ #
14
+ # Adding this to work around the mild inconvenience of the `app` container's
15
+ # entrypoint generating no output.
16
+ #
17
+ cat <<-'PROMPT'
18
+
19
+
20
+
21
+ 🎉 Dev Environment Initialized! 🎉
22
+
23
+ ℹ️ To use this environment, open a new terminal and run
24
+
25
+ dx/exec bash
26
+
27
+ 🕹 Use `ctrl-c` to exit.
28
+
29
+
30
+
31
+ PROMPT
32
+
33
+ # Using `sleep infinity` instead of `tail -f /dev/null`. This may be a
34
+ # performance improvement based on the conversation on a semi-related
35
+ # StackOverflow page.
36
+ #
37
+ # @see https://stackoverflow.com/a/41655546
38
+ sleep infinity
data/dx/start ADDED
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
6
+
7
+ . "${SCRIPT_DIR}/dx.sh.lib"
8
+ require_command "docker"
9
+ load_docker_compose_env
10
+
11
+ usage_on_help "Starts all services, including a container in which to run your app" "" "" "" "${@}"
12
+
13
+ log "🚀" "Starting docker-compose.dx.yml"
14
+
15
+ BUILD=--build
16
+ if [ "${1}" == "--no-build" ]; then
17
+ BUILD=
18
+ fi
19
+
20
+ docker \
21
+ compose \
22
+ --file docker-compose.dx.yml \
23
+ --project-name "${PROJECT_NAME}" \
24
+ --env-file "${ENV_FILE}" \
25
+ up \
26
+ "${BUILD}" \
27
+ --timestamps \
28
+ --force-recreate
29
+
30
+ # vim: ft=bash
data/dx/stop ADDED
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
6
+
7
+ . "${SCRIPT_DIR}/dx.sh.lib"
8
+ require_command "docker"
9
+ load_docker_compose_env
10
+
11
+ usage_on_help "Stops all services, the container in which to run your app and removes any volumes" "" "" "" "${@}"
12
+
13
+ log "🚀" "Stopping docker-compose.dx.yml"
14
+
15
+ docker \
16
+ compose \
17
+ --file docker-compose.dx.yml \
18
+ --project-name "${PROJECT_NAME}" \
19
+ --env-file "${ENV_FILE}" \
20
+ down \
21
+ --volumes
22
+
23
+ # vim: ft=bash
@@ -1,29 +1,40 @@
1
1
  module WithClues
2
2
  class BrowserLogs
3
- def dump(notifier, page:, context:)
3
+ def dump(notifier, page:, context:, captured_logs: [])
4
4
  if !page.respond_to?(:driver)
5
5
  notifier.notify "Something may be wrong. page (#{page.class}) does not respond to #driver"
6
6
  return
7
7
  end
8
8
  if page.driver.respond_to?(:browser)
9
- if page.driver.browser.respond_to?(:manage)
10
- if page.driver.browser.manage.respond_to?(:logs)
11
- logs = page.driver.browser.manage.logs
12
- browser_logs = logs.get(:browser)
13
- notifier.notify "BROWSER LOGS {"
14
- browser_logs.each do |log|
15
- notifier.notify_raw log.message
16
- end
17
- notifier.notify "} END BROWSER LOGS"
18
- else
19
- notifier.notify "NO BROWSER LOGS: page.driver.browser.manage #{page.driver.browser.manage.class} does not respond to #logs"
9
+ logs = locate_logs(page.driver.browser, notifier: notifier)
10
+ if !logs.nil?
11
+ browser_logs = logs.get(:browser)
12
+ notifier.notify "BROWSER LOGS {"
13
+ browser_logs.each do |log|
14
+ notifier.notify_raw log.message
20
15
  end
21
- else
22
- notifier.notify "NO BROWSER LOGS: page.driver.browser #{page.driver.browser.class} does not respond to #manage"
16
+ notifier.notify "} END BROWSER LOGS"
23
17
  end
24
18
  else
25
- notifier.notify "NO BROWSER LOGS: page.driver #{page.driver.class} does not respond to #browser"
19
+ notifier.notify "[with_clues: #{self.class}] NO BROWSER LOGS: page.driver #{page.driver.class} does not respond to #browser"
26
20
  end
27
21
  end
22
+
23
+ private
24
+
25
+ def locate_logs(browser, notifier:)
26
+ if browser.respond_to?(:logs)
27
+ return browser.logs
28
+ elsif browser.respond_to?(:manage)
29
+ if browser.manage.respond_to?(:logs)
30
+ return browser.manage.logs
31
+ end
32
+ notifier.notify "[with_clues: #{self.class}] NO BROWSER LOGS: page.driver.browser.manage #{browser.manage.class} does not respond to #logs"
33
+ else
34
+ notifier.notify "[with_clues: #{self.class}] NO BROWSER LOGS: page.driver.browser #{browser.class} does not respond to #manage or #logs"
35
+ end
36
+ nil
37
+ end
38
+
28
39
  end
29
40
  end
@@ -1,18 +1,31 @@
1
1
  module WithClues
2
2
  class Html
3
- def dump(notifier, page:, context:)
3
+ def dump(notifier, page:, context:, captured_logs: [])
4
+ access_page_html = if page.respond_to?(:html)
5
+ ->(page) { page.html }
6
+ elsif page.respond_to?(:content)
7
+ ->(page) { page.content }
8
+ elsif page.respond_to?(:native)
9
+ ->(page) { page.native }
10
+ else
11
+ notifier.notify "Something may be wrong. page (#{page.class}) does not respond to #html, #native, or #content"
12
+ return
13
+ end
4
14
  notifier.blank_line
5
15
  notifier.notify "HTML {"
6
16
  notifier.blank_line
7
- if page.respond_to?(:html)
8
- notifier.notify_raw page.html
9
- elsif page.respond_to?(:native)
10
- notifier.notify_raw page.native
11
- else
12
- notifier.notify "[!] Something may be wrong. page (#{page.class}) does not respond to #html or #native"
13
- end
17
+ notifier.notify_raw access_page_html.(page)
14
18
  notifier.blank_line
15
19
  notifier.notify "} END HTML"
20
+ if captured_logs.any?
21
+ notifier.notify "LOGS {"
22
+ notifier.blank_line
23
+ captured_logs.each do |log|
24
+ notifier.notify_raw log
25
+ end
26
+ notifier.blank_line
27
+ notifier.notify "} END LOGS"
28
+ end
16
29
  end
17
30
  end
18
31
  end
@@ -1,6 +1,7 @@
1
1
  require_relative "html"
2
2
  require_relative "browser_logs"
3
3
  require_relative "notifier"
4
+ require_relative "private/custom_clue_method_analysis"
4
5
 
5
6
  module WithClues
6
7
  module Method
@@ -16,6 +17,15 @@ module WithClues
16
17
  # unexpectedly failing
17
18
  def with_clues(context=nil, &block)
18
19
  notifier = WithClues::Notifier.new($stdout)
20
+ captured_logs = []
21
+ if defined?(page) && page.respond_to?(:on)
22
+ begin
23
+ page.on("console", ->(msg) { captured_logs << msg.text })
24
+ rescue => ex
25
+ raise ex
26
+ notifier.notify "'page' was defined and responds to #on, however invoking it generated an exception: #{ex}"
27
+ end
28
+ end
19
29
  block.()
20
30
  notifier.notify "A passing test has been wrapped with `with_clues`. You should remove the call to `with_clues`"
21
31
  rescue Exception => ex
@@ -23,18 +33,25 @@ module WithClues
23
33
  @@clue_classes[:custom].each do |klass|
24
34
  klass.new.dump(notifier, context: context)
25
35
  end
26
- if !defined?(page)
27
- raise ex
28
- end
29
- notifier.notify "Test failed: #{ex.message}"
30
- @@clue_classes[:require_page].each do |klass|
31
- klass.new.dump(notifier, context: context, page: page)
36
+ if defined?(page)
37
+ notifier.notify "Test failed: #{ex.message}"
38
+ @@clue_classes[:require_page].each do |klass|
39
+ klass.new.dump(notifier, context: context, page: page, captured_logs: captured_logs)
40
+ end
32
41
  end
33
42
  raise ex
34
43
  end
35
44
 
36
45
  def self.use_custom_clue(klass)
37
- @@clue_classes[:custom] << klass
46
+ dump_method = klass.instance_method(:dump)
47
+ analysis = WithClues::Private::CustomClueMethodAnalysis.from_method(dump_method)
48
+ if analysis.standard_implementation?
49
+ @@clue_classes[:custom] << klass
50
+ elsif analysis.requires_page_object?
51
+ @@clue_classes[:require_page] << klass
52
+ else
53
+ analysis.raise_exception!
54
+ end
38
55
  end
39
56
  end
40
57
  end
@@ -0,0 +1,149 @@
1
+ module WithClues
2
+ module Private
3
+ class CustomClueMethodAnalysis
4
+
5
+ def self.from_method(unbound_method)
6
+
7
+ params = unbound_method.parameters.map { |param_array| Param.new(param_array) }
8
+
9
+ if params.size == 2
10
+ two_arg_method = TwoArgMethod.new(params)
11
+ if two_arg_method.valid?
12
+ return StandardImplementation.new
13
+ end
14
+
15
+ return BadParams.new(two_arg_method.errors)
16
+
17
+ elsif params.size == 4
18
+ three_arg_method = FourArgMethod.new(params)
19
+ if three_arg_method.valid?
20
+ return RequiresPageObject.new
21
+ end
22
+ return BadParams.new(three_arg_method.errors)
23
+ end
24
+
25
+ BadParams.new([
26
+ "dump (#{unbound_method.owner}) accepted #{params.size} arguments, not 2 or 4. Got: #{params.map(&:to_s).join(", ")}, should be one positional and either one keyword arg named 'context:' or three keyword args named 'context:', 'page:', and 'captured_logs:'"
27
+ ])
28
+ end
29
+
30
+ def standard_implementation?
31
+ false
32
+ end
33
+
34
+ def requires_page_object?
35
+ false
36
+ end
37
+
38
+ def raise_exception!
39
+ raise StandardError.new("Unimplemented condition found inside #from_method")
40
+ end
41
+
42
+ class Param
43
+
44
+ def initialize(method_param_array)
45
+ @type = method_param_array[0]
46
+ @name = method_param_array[1]
47
+
48
+ end
49
+
50
+ def required?
51
+ @type == :req
52
+ end
53
+ def keyword_required?
54
+ @type == :keyreq
55
+ end
56
+
57
+ def named?(*allowed_names)
58
+ allowed_names.include?(@name)
59
+ end
60
+ def name
61
+ if self.keyword_required?
62
+ "#{@name}:"
63
+ else
64
+ @name
65
+ end
66
+ end
67
+ def to_s
68
+ "#<#{self.class} #{name}/#{@type}"
69
+ end
70
+ end
71
+
72
+ class TwoArgMethod
73
+ attr_reader :errors
74
+ def initialize(params)
75
+ @errors = []
76
+ if !params[0].required?
77
+ @errors << "Param 1, #{params[0].name}, is not required"
78
+ end
79
+ require_keyword(2,params[1])
80
+ end
81
+
82
+ def valid?
83
+ @errors.empty?
84
+ end
85
+ private
86
+
87
+ def require_keyword(param_number, param)
88
+ if !param.keyword_required?
89
+ @errors << "Param #{param_number}, #{param.name}, is not a required keyword param"
90
+ end
91
+ if !param.named?(*allowed_names)
92
+ @errors << "Param #{param_number}, #{param.name}, should be one of #{allowed_names.join(',')}"
93
+ end
94
+ end
95
+
96
+ def allowed_names
97
+ [ :context ]
98
+ end
99
+ end
100
+
101
+ class FourArgMethod < TwoArgMethod
102
+ def initialize(params)
103
+ super(params)
104
+ require_keyword(3,params[2])
105
+ require_keyword(4,params[3])
106
+ end
107
+ private
108
+ def allowed_names
109
+ [ :context, :page, :captured_logs ]
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+ class GoodParams < CustomClueMethodAnalysis
116
+ def raise_exception!
117
+ raise StandardError.new("You should not have called .exception on a #{self.class.name}")
118
+ end
119
+ end
120
+
121
+ class RequiresPageObject < CustomClueMethodAnalysis
122
+ def requires_page_object?
123
+ true
124
+ end
125
+ end
126
+
127
+ class StandardImplementation < CustomClueMethodAnalysis
128
+ def standard_implementation?
129
+ true
130
+ end
131
+ end
132
+
133
+ class BadParams < CustomClueMethodAnalysis
134
+ def initialize(errors)
135
+ if errors.empty?
136
+ raise ArgumentError,"BadParams requires errors"
137
+ else
138
+ @message = errors.map(&:to_s).join(", ")
139
+ end
140
+ end
141
+
142
+ DEFAULT_ERROR = "dump must take one required param, one keyword param named context: and an optional keyword param named page:"
143
+
144
+ def raise_exception!
145
+ raise NameError.new(@message)
146
+ end
147
+ end
148
+ end
149
+ end
@@ -1,3 +1,3 @@
1
1
  module WithClues
2
- VERSION="1.1.0"
2
+ VERSION="1.3.0"
3
3
  end
data/with_clues.gemspec CHANGED
@@ -1,13 +1,11 @@
1
1
  require_relative "lib/with_clues/version"
2
2
 
3
- #require_relative "lib/«gem»/version"
4
-
5
3
  Gem::Specification.new do |spec|
6
4
  spec.name = "with_clues"
7
5
  spec.version = WithClues::VERSION
8
6
  spec.authors = ["Dave Copeland"]
9
7
  spec.email = ["davec@naildrivin5.com"]
10
- spec.summary = %q{WTF does this do?}
8
+ spec.summary = %q{Temporarily add context to failing tests to get more information, such as what HTML was being examined when a browser-based test fails.}
11
9
  spec.homepage = "https://sustainable-rails.com"
12
10
  spec.license = "Hippocratic"
13
11
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: with_clues
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dave Copeland
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-04 00:00:00.000000000 Z
11
+ date: 2024-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -73,28 +73,39 @@ executables: []
73
73
  extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
- - ".circleci/config.yml"
77
76
  - ".gitignore"
78
77
  - ".rspec"
79
78
  - ".tool-versions"
79
+ - CHANGELOG.md
80
80
  - CODE_OF_CONDUCT.md
81
81
  - CONTRIBUTING.md
82
+ - Dockerfile.dx
82
83
  - Gemfile
83
84
  - LICENSE.md
84
85
  - README.md
85
86
  - Rakefile
86
87
  - bin/ci
87
88
  - bin/console
88
- - bin/mk_circle_config
89
- - bin/mk_gem
89
+ - bin/matrix
90
90
  - bin/rake
91
91
  - bin/rspec
92
92
  - bin/setup
93
+ - docker-compose.dx.yml
94
+ - dx/build
95
+ - dx/docker-compose.env
96
+ - dx/dx.sh.lib
97
+ - dx/exec
98
+ - dx/prune
99
+ - dx/setupkit.sh.lib
100
+ - dx/show-help-in-app-container-then-wait.sh
101
+ - dx/start
102
+ - dx/stop
93
103
  - lib/with_clues.rb
94
104
  - lib/with_clues/browser_logs.rb
95
105
  - lib/with_clues/html.rb
96
106
  - lib/with_clues/method.rb
97
107
  - lib/with_clues/notifier.rb
108
+ - lib/with_clues/private/custom_clue_method_analysis.rb
98
109
  - lib/with_clues/version.rb
99
110
  - with_clues.gemspec
100
111
  homepage: https://sustainable-rails.com
@@ -119,8 +130,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
130
  - !ruby/object:Gem::Version
120
131
  version: '0'
121
132
  requirements: []
122
- rubygems_version: 3.3.3
133
+ rubygems_version: 3.5.21
123
134
  signing_key:
124
135
  specification_version: 4
125
- summary: WTF does this do?
136
+ summary: Temporarily add context to failing tests to get more information, such as
137
+ what HTML was being examined when a browser-based test fails.
126
138
  test_files: []
data/.circleci/config.yml DELETED
@@ -1,98 +0,0 @@
1
- # THIS IS GENERATED - DO NOT EDIT
2
- # regenerate with bin/mk_circle_config
3
- # You are very welcome
4
- ---
5
- version: '2.1'
6
- jobs:
7
- ruby__2_6:
8
- docker:
9
- - image: cimg/ruby:2.6
10
- steps:
11
- - checkout
12
- - run:
13
- name: Setup for build
14
- command: bin/setup
15
- - run:
16
- name: Ensure bin/setup is idempotent
17
- command: bin/setup
18
- - run:
19
- name: Create the test results dir
20
- command: mkdir -p /tmp/test-results/2.6
21
- - run:
22
- name: Run all tests
23
- command: bin/ci /tmp/test-results/2.6/rspec_results.xml
24
- - store_test_results:
25
- path: "/tmp/test-results/2.6"
26
- - store_artifacts:
27
- path: "/tmp/test-results/2.6"
28
- ruby__2_7:
29
- docker:
30
- - image: cimg/ruby:2.7
31
- steps:
32
- - checkout
33
- - run:
34
- name: Setup for build
35
- command: bin/setup
36
- - run:
37
- name: Ensure bin/setup is idempotent
38
- command: bin/setup
39
- - run:
40
- name: Create the test results dir
41
- command: mkdir -p /tmp/test-results/2.7
42
- - run:
43
- name: Run all tests
44
- command: bin/ci /tmp/test-results/2.7/rspec_results.xml
45
- - store_test_results:
46
- path: "/tmp/test-results/2.7"
47
- - store_artifacts:
48
- path: "/tmp/test-results/2.7"
49
- ruby__3_0:
50
- docker:
51
- - image: cimg/ruby:3.0
52
- steps:
53
- - checkout
54
- - run:
55
- name: Setup for build
56
- command: bin/setup
57
- - run:
58
- name: Ensure bin/setup is idempotent
59
- command: bin/setup
60
- - run:
61
- name: Create the test results dir
62
- command: mkdir -p /tmp/test-results/3.0
63
- - run:
64
- name: Run all tests
65
- command: bin/ci /tmp/test-results/3.0/rspec_results.xml
66
- - store_test_results:
67
- path: "/tmp/test-results/3.0"
68
- - store_artifacts:
69
- path: "/tmp/test-results/3.0"
70
- ruby__3_1:
71
- docker:
72
- - image: cimg/ruby:3.1
73
- steps:
74
- - checkout
75
- - run:
76
- name: Setup for build
77
- command: bin/setup
78
- - run:
79
- name: Ensure bin/setup is idempotent
80
- command: bin/setup
81
- - run:
82
- name: Create the test results dir
83
- command: mkdir -p /tmp/test-results/3.1
84
- - run:
85
- name: Run all tests
86
- command: bin/ci /tmp/test-results/3.1/rspec_results.xml
87
- - store_test_results:
88
- path: "/tmp/test-results/3.1"
89
- - store_artifacts:
90
- path: "/tmp/test-results/3.1"
91
- workflows:
92
- version: 2
93
- all_rubies:
94
- jobs:
95
- - ruby__2_6
96
- - ruby__2_7
97
- - ruby__3_0
98
- - ruby__3_1
data/bin/mk_circle_config DELETED
@@ -1,84 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "yaml"
4
- require "pathname"
5
-
6
- circle_config = {
7
- "version" => "2.1",
8
- "jobs" => {},
9
- "workflows" => {
10
- "version" => 2,
11
- "all_rubies" => {
12
- "jobs" => [
13
- ],
14
- },
15
- }
16
- }
17
-
18
- supported_rubies = [
19
- "2.6",
20
- "2.7",
21
- "3.0",
22
- "3.1",
23
- ]
24
-
25
- supported_rubies.each do |ruby_verison|
26
-
27
- test_results_dir = "/tmp/test-results/#{ruby_verison}"
28
- job_name = "ruby__#{ruby_verison.gsub(/\./,"_")}"
29
-
30
- job = {
31
- "docker" => [
32
- {
33
- "image" => "cimg/ruby:#{ruby_verison}",
34
- }
35
- ],
36
- "steps" => [
37
- "checkout",
38
- {
39
- "run" => {
40
- "name" => "Setup for build",
41
- "command" => "bin/setup",
42
- }
43
- },
44
- {
45
- "run" => {
46
- "name" => "Ensure bin/setup is idempotent",
47
- "command" => "bin/setup",
48
- }
49
- },
50
- {
51
- "run" => {
52
- "name" => "Create the test results dir",
53
- "command" => "mkdir -p #{test_results_dir}",
54
- }
55
- },
56
- {
57
- "run" => {
58
- "name" => "Run all tests",
59
- "command" => "bin/ci #{test_results_dir}/rspec_results.xml",
60
- }
61
- },
62
- {
63
- "store_test_results" => {
64
- "path" => test_results_dir,
65
- }
66
- },
67
- {
68
- "store_artifacts" => {
69
- "path" => test_results_dir,
70
- }
71
- },
72
- ]
73
- }
74
- circle_config["jobs"][job_name] = job
75
- circle_config["workflows"]["all_rubies"]["jobs"] << job_name
76
- end
77
-
78
- circle_config_file = (Pathname(__FILE__).dirname / ".." / ".circleci" / "config.yml").expand_path
79
- File.open(circle_config_file,"w") do |file|
80
- file.puts "# THIS IS GENERATED - DO NOT EDIT"
81
- file.puts "# regenerate with bin/mk_circle_config"
82
- file.puts "# You are very welcome"
83
- file.puts circle_config.to_yaml
84
- end
data/bin/mk_gem DELETED
@@ -1,73 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "fileutils"
4
- require "pathname"
5
-
6
- include FileUtils
7
-
8
- gem_name = ARGV[0]
9
-
10
- if gem_name.nil?
11
- puts "usage: #{$0} gem_name"
12
- exit 1
13
- end
14
-
15
- root = Pathname(__FILE__).dirname / ".."
16
-
17
- module_name = gem_name.split(/_/).map { |part|
18
- part.capitalize
19
- }.join
20
-
21
- mkdir_p root / "lib"
22
-
23
- File.open(root / "lib" / "#{gem_name}.rb", "w") do |file|
24
- file.puts "module #{module_name}"
25
- file.puts "end"
26
- end
27
-
28
- mkdir_p root / "lib" / gem_name
29
-
30
- File.open(root / "lib" / gem_name / "version.rb", "w") do |file|
31
- file.puts "module #{module_name}"
32
- file.puts " VERSION=\"1.0.0\""
33
- file.puts "end"
34
- end
35
-
36
- gemspec = File.read("rubygem.gemspec")
37
- File.open(root / "#{gem_name}.gemspec","w") do |file|
38
- file.puts "require_relative \"lib/#{gem_name}/version\""
39
- file.puts
40
- gemspec.split(/\n/).each do |line|
41
- if line =~ /^\s*spec.name/
42
- file.puts " spec.name = \"#{gem_name}\""
43
- elsif line =~ /^\s*spec.version/
44
- file.puts " spec.version = #{module_name}::VERSION"
45
- elsif line.include?("«gem_name»")
46
- file.puts line.gsub(/«gem_name»/,gem_name)
47
- else
48
- file.puts line
49
- end
50
- end
51
- end
52
-
53
- license = File.read(root / "LICENSE.md")
54
- File.open(root / "LICENSE.md","w") do |file|
55
- license.split(/\n/).each_with_index do |line,index|
56
- if index == 0
57
- file.puts "[#{gem_name}] Copyright (2021) (David Copeland)(“Licensor”)"
58
- else
59
- file.puts line
60
- end
61
- end
62
- end
63
-
64
- readme = File.read(root / "README.md")
65
- File.open(root / "README.md","w") do |file|
66
- license.split(/\n/).each_with_index do |line,index|
67
- if index == 0
68
- file.puts "# #{gem_name} - does a thing"
69
- else
70
- file.puts line
71
- end
72
- end
73
- end