test-recorder 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2df7b7454c1776d950afaadc9fcefdab037f2638a0a0f05ba7251ef08131428
4
- data.tar.gz: 97e45933561090c772aef6a26849075626c7d8e9766d0e25c60a642841a38f36
3
+ metadata.gz: 10ed1ca4c5868a640ac26f54969374fabb4d298f1e25a63fbf898b147410c97a
4
+ data.tar.gz: 36c717726cf643f138140f04d1c6068fc59f04a93d9d3740bc219f4c6fabc6eb
5
5
  SHA512:
6
- metadata.gz: 2ced41859d336565547e55de99b4574952f175bc3acb7ce6a6231c07ce32bab27483689f6a9c3967950d4da80145ea2002a0abe2ba3b6905c56fa9728902e004
7
- data.tar.gz: 1657c90f69bcf393bb7e4905fde1a9e6afc74b7c5e864521a72bee0f4d3f97db9b1ceba28e466d321e7122ce2545548431ccd593a576e53f2b7c3c45d34214da
6
+ metadata.gz: 0e227207116c5b6bef3acd692d755499117665b1e774cefdaf8e9f2722dc62865997f7bc2246e64ec54b6b8e2f18992cadd8b49b510ff11702a6d37b0a5b1fe4
7
+ data.tar.gz: 7895ee63692f785d0633e0b35ea2d4d104a318d5bc6f4a27501c4294764367c77c9902d912cd8635976c9f862e5950819fb37f46a7beea09f336fee910eb6989
@@ -18,6 +18,13 @@ jobs:
18
18
  strategy:
19
19
  matrix:
20
20
  ruby: [ '4.0', '3.4', '3.3' ]
21
+ chrome: [ 'stable' ]
22
+ include:
23
+ # The last version without Page.startScreenRecording, to cover the screencast fallback.
24
+ - ruby: '4.0'
25
+ chrome: '152'
26
+
27
+ name: build (ruby ${{ matrix.ruby }}, chrome ${{ matrix.chrome }})
21
28
 
22
29
  steps:
23
30
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -43,5 +50,13 @@ jobs:
43
50
  run: |
44
51
  chmod +x "$HOME/.local/bin/ffmpeg"
45
52
  echo "$HOME/.local/bin" >> "$GITHUB_PATH"
53
+ - name: Use Chrome ${{ matrix.chrome }}
54
+ if: matrix.chrome != 'stable'
55
+ run: |
56
+ install() { npx -y @puppeteer/browsers install "$1@${{ matrix.chrome }}" --path "$RUNNER_TEMP/browsers" | tail -n 1 | cut -d ' ' -f 2; }
57
+ echo "CHROME_BIN=$(install chrome)" >> "$GITHUB_ENV"
58
+ echo "SE_CHROMEDRIVER=$(install chromedriver)" >> "$GITHUB_ENV"
59
+ # Chrome for Testing is not installed under /opt, so AppArmor blocks its sandbox.
60
+ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
46
61
  - name: Run test
47
62
  run: bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 0.5.0 - 2026-10-06
4
+
5
+ * Record with `Page.startScreenRecording` on Chrome 153+. Chrome encodes the video itself as an `.mp4` file, so FFmpeg is no longer needed. Older Chrome versions fall back to the screencast (`.webm`, requires FFmpeg). `TestRecorder.frame_rate` (default: 25) sets the frame rate, and `jpeg_quality` and `every_nth_frame` now only apply to the screencast fallback
6
+
3
7
  ## 0.4.0 - 2026-08-30
4
8
 
5
9
  * Record each frame with its own timestamp so that videos play back at the same speed as the test
data/README.md CHANGED
@@ -9,7 +9,9 @@ This gem was inspired by [Record video feature of Playwright](https://playwright
9
9
 
10
10
  ## Requirements
11
11
 
12
- This gem depends on FFmpeg. Please install that package.
12
+ Chrome 153 or later records the video by itself (using `Page.startScreenRecording`), so nothing else is needed.
13
+
14
+ With older Chrome versions, this gem falls back to the screencast, and depends on FFmpeg for that. Please install that package.
13
15
 
14
16
  On Debian/Ubuntu:
15
17
 
@@ -17,6 +19,13 @@ On Debian/Ubuntu:
17
19
  sudo apt-get install ffmpeg
18
20
  ```
19
21
 
22
+ ## Output format
23
+
24
+ The videos are generated in `tmp/videos` directory.
25
+
26
+ * Chrome 153 or later: `.mp4` (AV1)
27
+ * Older Chrome versions: `.webm` (VP8)
28
+
20
29
  ## Supported libraries
21
30
 
22
31
  Rails system tests and RSpec(System Spec and Feature Spec).
@@ -98,11 +107,17 @@ end
98
107
 
99
108
  ### Configuration
100
109
 
101
- You can change the recording quality and the max width/height (in pixels).
110
+ You can change the max width/height (in pixels) and the frame rate. These apply to both recording methods.
102
111
 
103
112
  ```ruby
104
- TestRecorder.jpeg_quality = 80 # default: 60
105
113
  TestRecorder.max_dimension = 1280 # default: 1000
114
+ TestRecorder.frame_rate = 30 # default: 25
115
+ ```
116
+
117
+ The following settings only apply to the screencast fallback used with Chrome versions older than 153. They are ignored when Chrome records the screen by itself.
118
+
119
+ ```ruby
120
+ TestRecorder.jpeg_quality = 80 # default: 60
106
121
  ```
107
122
 
108
123
  You can also record only every Nth frame. This lowers the recording overhead
@@ -1,72 +1,31 @@
1
- require "base64"
2
1
  require "fileutils"
3
- require "tempfile"
2
+ require "selenium-webdriver"
4
3
 
5
- require "test_recorder/frame_writer"
4
+ require "test_recorder/screen_recording"
5
+ require "test_recorder/screencast"
6
6
 
7
7
  module TestRecorder
8
8
  class CdpRecorder
9
- FFMPEG_ENCODE_OPTIONS = %w[-y -an -r 25 -c:v vp8 -qmin 0 -qmax 50 -crf 8 -deadline realtime -speed 8 -b:v 1M -threads 1].freeze
10
-
11
- # ffmpeg's -t treats the given duration as an exclusive cutoff, dropping the very frame that
12
- # marks the video's real length if it lands exactly on it. This nudges -t just past that frame
13
- # so it is kept, without adding a duration long enough to be noticeable.
14
- FFMPEG_DURATION_SLACK_S = 0.01
15
-
16
- Record = Struct.new(:page, :io, :last_metadata_time, :last_metadata_received_at)
17
-
18
9
  class << self
19
- def record(devtools)
20
- records[devtools] ||= begin
21
- page = devtools.page
22
- page.enable
23
-
24
- record = Record.new(page)
25
- page.on(:screencast_frame) do |event|
26
- write_frame(record, event)
27
- record.page.screencast_frame_ack(session_id: event["sessionId"])
28
- end
29
- record
30
- end
10
+ def screen_recording_supported?(browser)
11
+ support.fetch(browser) { support[browser] = probe_screen_recording(browser) }
31
12
  end
32
13
 
33
14
  private
34
15
 
35
- def write_frame(record, event)
36
- record.io&.write(Base64.decode64(event["data"]), frame_time(record, event))
37
- rescue IOError
38
- # A frame can still arrive after the recording was stopped and the file was closed.
39
- rescue => e
40
- warn "[TestRecorder] Failed to write a screencast frame: #{e.class}: #{e.message}"
41
- end
42
-
43
- # `metadata.timestamp` is the wall clock time of the frame swap, in seconds, on the browser's
44
- # clock. The protocol marks it optional, so when it is missing, estimate it from the last
45
- # frame that did carry one plus how much monotonic time has passed since. Falling back to
46
- # this process's own wall clock instead would silently distort pacing whenever the browser
47
- # runs on a different host than the driver, since the two wall clocks are not guaranteed to
48
- # agree.
49
- def frame_time(record, event)
50
- metadata = event["metadata"]
51
- timestamp = metadata && metadata["timestamp"]
52
-
53
- if timestamp
54
- record.last_metadata_time = timestamp
55
- record.last_metadata_received_at = monotonic_time
56
- timestamp
57
- elsif record.last_metadata_time
58
- record.last_metadata_time + (monotonic_time - record.last_metadata_received_at)
59
- else
60
- Time.now.to_f
61
- end
16
+ # Stopping when nothing is being recorded tells whether the command exists without starting
17
+ # anything: Chrome 153+ answers "No active screen recording", older versions do not know it.
18
+ def probe_screen_recording(browser)
19
+ browser.execute_cdp("Page.stopScreenRecording")
20
+ true
21
+ rescue Selenium::WebDriver::Error::UnknownCommandError
22
+ false
23
+ rescue Selenium::WebDriver::Error::WebDriverError
24
+ true
62
25
  end
63
26
 
64
- def monotonic_time
65
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
- end
67
-
68
- def records
69
- @records ||= {}.compare_by_identity
27
+ def support
28
+ @support ||= {}.compare_by_identity
70
29
  end
71
30
  end
72
31
 
@@ -80,63 +39,41 @@ module TestRecorder
80
39
  @started = enabled
81
40
  return unless @started
82
41
 
83
- @tmp_video = Tempfile.new(["testrecorder", ".mkv"])
84
- @tmp_video.binmode
85
-
86
- @record = self.class.record(page.driver.browser.devtools)
87
- @frame_writer = FrameWriter.new(@tmp_video)
88
- @record.io = @frame_writer
89
-
90
- @record.page.start_screencast(format: "jpeg", quality: TestRecorder.jpeg_quality, max_width: TestRecorder.max_dimension, max_height: TestRecorder.max_dimension, every_nth_frame: TestRecorder.every_nth_frame)
42
+ browser = page.driver.browser
43
+ @recording = if self.class.screen_recording_supported?(browser)
44
+ ScreenRecording.new(browser)
45
+ else
46
+ Screencast.new(browser)
47
+ end
48
+ @recording.start
49
+ rescue => e
50
+ warn "[TestRecorder] Failed to start the recording: #{e.class}: #{e.message}"
51
+ @started = false
91
52
  end
92
53
 
93
54
  def stop_and_discard
94
55
  return unless @started
95
56
 
96
- @record.io = nil
97
- @record.page.stop_screencast
98
- @tmp_video.close!
57
+ @recording.stop_and_discard
58
+ rescue => e
59
+ warn "[TestRecorder] Failed to stop the recording: #{e.class}: #{e.message}"
99
60
  end
100
61
 
101
- def stop_and_save(filename)
62
+ # `name` is the file name without an extension, since the format depends on how it was recorded.
63
+ # Returns the path of the video, or "" when nothing was saved.
64
+ def stop_and_save(name)
102
65
  return "" unless @started
103
66
 
104
- @record.io = nil
105
- @record.page.stop_screencast
106
- @frame_writer.finish
107
- @tmp_video.flush
108
-
109
- # Chrome sends a frame as soon as the screencast starts, so a test that fails
110
- # before it draws anything still leaves behind the one frame of the blank page
111
- # it started on.
112
- if @frame_writer.frame_count < 2
113
- warn "[TestRecorder] The screencast captured no page updates, so no video was saved."
114
- @tmp_video.close!
115
- return ""
116
- end
117
-
118
67
  video_dir = ::Rails.root.join("tmp", "videos")
119
68
  FileUtils.mkdir_p(video_dir)
120
- video_path = video_dir.join(filename).to_s
121
-
122
- # Each frame carries its own timestamp in the Matroska stream, so ffmpeg knows how long to
123
- # hold it and duplicates frames to reach the constant output frame rate on its own. Without a
124
- # known end, though, ffmpeg pads however long it likes past the last real timestamp, so -t
125
- # caps the output at the video's real duration explicitly.
126
- duration = @frame_writer.duration + FFMPEG_DURATION_SLACK_S
127
- result = system("ffmpeg", "-loglevel", "error", "-f", "matroska", "-i", @tmp_video.path, "-t", duration.to_s, *FFMPEG_ENCODE_OPTIONS, video_path)
128
-
129
- @tmp_video.close!
130
-
131
- if result.nil?
132
- warn "[TestRecorder] Failed to execute ffmpeg. Please make sure that FFmpeg is installed."
133
- return ""
134
- elsif !result
135
- warn "[TestRecorder] ffmpeg failed to encode #{video_path}."
136
- return ""
137
- end
138
-
139
- video_path
69
+ video_path = video_dir.join("#{name}#{@recording.extension}").to_s
70
+
71
+ @recording.stop_and_save(video_path) ? video_path : ""
72
+ rescue => e
73
+ # The browser can be gone by now (a crash, a closed window). The test's own failure is what
74
+ # matters, so this must not replace it with an error from here.
75
+ warn "[TestRecorder] Failed to save the recording: #{e.class}: #{e.message}"
76
+ ""
140
77
  end
141
78
  end
142
79
  end
@@ -11,7 +11,7 @@ module TestRecorder
11
11
 
12
12
  def before_teardown
13
13
  if failures.any? { |failure| !failure.is_a?(Minitest::Skip) }
14
- video_path = @cdp_recorder.stop_and_save("failures_#{TestRecorder.sanitize_filename(self.name)}.webm")
14
+ video_path = @cdp_recorder.stop_and_save("failures_#{TestRecorder.sanitize_filename(self.name)}")
15
15
  puts "[Video]: #{video_path}" if File.exist?(video_path)
16
16
  else
17
17
  @cdp_recorder.stop_and_discard
@@ -11,7 +11,7 @@ module TestRecorder
11
11
  if passed?(example)
12
12
  cdp_recorder.stop_and_discard
13
13
  else
14
- video_path = cdp_recorder.stop_and_save("failures_#{method_name(example)}.webm").to_s
14
+ video_path = cdp_recorder.stop_and_save("failures_#{method_name(example)}").to_s
15
15
  if File.exist?(video_path)
16
16
  example.metadata[:extra_failure_lines] = [example.metadata[:extra_failure_lines], "[Video]: #{video_path}"].flatten
17
17
  end
@@ -0,0 +1,93 @@
1
+ require "base64"
2
+
3
+ module TestRecorder
4
+ # Records with `Page.startScreenRecording` (Chrome 153+). Chrome captures the page at a fixed
5
+ # frame rate and encodes an MP4 itself, so nothing needs to be encoded on this side. The commands
6
+ # are sent with `execute_cdp`, which does not need the selenium-devtools gem.
7
+ class ScreenRecording
8
+ EXTENSION = ".mp4".freeze
9
+
10
+ # The size of one `IO.read`.
11
+ READ_SIZE = 1 << 20
12
+
13
+ # What a new Chrome session shows before the test visits anything ("data:," is what
14
+ # chromedriver opens with).
15
+ BLANK_URLS = ["data:,", "about:blank"].freeze
16
+
17
+ def initialize(browser)
18
+ @browser = browser
19
+ end
20
+
21
+ def extension
22
+ EXTENSION
23
+ end
24
+
25
+ def start
26
+ @stream = @browser.execute_cdp("Page.startScreenRecording", maxWidth: TestRecorder.max_dimension, maxHeight: TestRecorder.max_dimension, frameRate: TestRecorder.frame_rate)["stream"]
27
+ end
28
+
29
+ def stop_and_discard
30
+ stop
31
+ ensure
32
+ close_stream
33
+ end
34
+
35
+ # Returns true when the video was written to `video_path`.
36
+ def stop_and_save(video_path)
37
+ # Chrome captures at a fixed frame rate whether or not anything is drawn, so a test that
38
+ # fails before it visits a page would still leave behind a video of the blank page it
39
+ # started on.
40
+ if blank_page?
41
+ stop
42
+ warn "[TestRecorder] The test failed before any page was loaded, so no video was saved."
43
+ return false
44
+ end
45
+
46
+ stop
47
+ read_stream_into(video_path)
48
+
49
+ if File.zero?(video_path)
50
+ warn "[TestRecorder] The screen recording was empty, so no video was saved."
51
+ File.delete(video_path)
52
+ return false
53
+ end
54
+
55
+ true
56
+ rescue
57
+ File.delete(video_path) if File.exist?(video_path)
58
+ raise
59
+ ensure
60
+ close_stream
61
+ end
62
+
63
+ private
64
+
65
+ def blank_page?
66
+ BLANK_URLS.include?(@browser.current_url)
67
+ rescue
68
+ false
69
+ end
70
+
71
+ # The stream stays readable after this, so it has to be closed even if nobody reads it.
72
+ def stop
73
+ @browser.execute_cdp("Page.stopScreenRecording")
74
+ end
75
+
76
+ def read_stream_into(video_path)
77
+ File.open(video_path, "wb") do |file|
78
+ loop do
79
+ chunk = @browser.execute_cdp("IO.read", handle: @stream, size: READ_SIZE)
80
+ file.write(chunk["base64Encoded"] ? Base64.decode64(chunk["data"]) : chunk["data"])
81
+ break if chunk["eof"]
82
+ end
83
+ end
84
+ end
85
+
86
+ # Runs on the way out of a failed stop or read too, so its own error must not hide that one.
87
+ def close_stream
88
+ @browser.execute_cdp("IO.close", handle: @stream)
89
+ rescue => e
90
+ warn "[TestRecorder] Failed to close the screen recording stream: #{e.class}: #{e.message}"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,138 @@
1
+ require "base64"
2
+ require "tempfile"
3
+
4
+ require "test_recorder/frame_writer"
5
+
6
+ module TestRecorder
7
+ # Records with `Page.startScreencast`, which sends a JPEG for every repaint, and has ffmpeg
8
+ # encode them into a video afterwards. This is the fallback for Chrome versions without
9
+ # `Page.startScreenRecording`.
10
+ class Screencast
11
+ EXTENSION = ".webm".freeze
12
+
13
+ FFMPEG_ENCODE_OPTIONS = %w[-y -an -c:v vp8 -qmin 0 -qmax 50 -crf 8 -deadline realtime -speed 8 -b:v 1M -threads 1].freeze
14
+
15
+ # ffmpeg's -t treats the given duration as an exclusive cutoff, dropping the very frame that
16
+ # marks the video's real length if it lands exactly on it. This nudges -t just past that frame
17
+ # so it is kept, without adding a duration long enough to be noticeable.
18
+ FFMPEG_DURATION_SLACK_S = 0.01
19
+
20
+ Record = Struct.new(:page, :io, :last_metadata_time, :last_metadata_received_at)
21
+
22
+ class << self
23
+ def record(devtools)
24
+ records[devtools] ||= begin
25
+ page = devtools.page
26
+ page.enable
27
+
28
+ record = Record.new(page)
29
+ page.on(:screencast_frame) do |event|
30
+ write_frame(record, event)
31
+ record.page.screencast_frame_ack(session_id: event["sessionId"])
32
+ end
33
+ record
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def write_frame(record, event)
40
+ record.io&.write(Base64.decode64(event["data"]), frame_time(record, event))
41
+ rescue IOError
42
+ # A frame can still arrive after the recording was stopped and the file was closed.
43
+ rescue => e
44
+ warn "[TestRecorder] Failed to write a screencast frame: #{e.class}: #{e.message}"
45
+ end
46
+
47
+ # `metadata.timestamp` is the wall clock time of the frame swap, in seconds, on the browser's
48
+ # clock. The protocol marks it optional, so when it is missing, estimate it from the last
49
+ # frame that did carry one plus how much monotonic time has passed since. Falling back to
50
+ # this process's own wall clock instead would silently distort pacing whenever the browser
51
+ # runs on a different host than the driver, since the two wall clocks are not guaranteed to
52
+ # agree.
53
+ def frame_time(record, event)
54
+ metadata = event["metadata"]
55
+ timestamp = metadata && metadata["timestamp"]
56
+
57
+ if timestamp
58
+ record.last_metadata_time = timestamp
59
+ record.last_metadata_received_at = monotonic_time
60
+ timestamp
61
+ elsif record.last_metadata_time
62
+ record.last_metadata_time + (monotonic_time - record.last_metadata_received_at)
63
+ else
64
+ Time.now.to_f
65
+ end
66
+ end
67
+
68
+ def monotonic_time
69
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
70
+ end
71
+
72
+ def records
73
+ @records ||= {}.compare_by_identity
74
+ end
75
+ end
76
+
77
+ def initialize(browser)
78
+ @browser = browser
79
+ end
80
+
81
+ def extension
82
+ EXTENSION
83
+ end
84
+
85
+ def start
86
+ @tmp_video = Tempfile.new(["testrecorder", ".mkv"])
87
+ @tmp_video.binmode
88
+
89
+ @record = self.class.record(@browser.devtools)
90
+ @frame_writer = FrameWriter.new(@tmp_video)
91
+ @record.io = @frame_writer
92
+
93
+ @record.page.start_screencast(format: "jpeg", quality: TestRecorder.jpeg_quality, max_width: TestRecorder.max_dimension, max_height: TestRecorder.max_dimension, every_nth_frame: TestRecorder.every_nth_frame)
94
+ end
95
+
96
+ def stop_and_discard
97
+ @record.io = nil
98
+ @record.page.stop_screencast
99
+ @tmp_video.close!
100
+ end
101
+
102
+ # Returns true when the video was written to `video_path`.
103
+ def stop_and_save(video_path)
104
+ @record.io = nil
105
+ @record.page.stop_screencast
106
+ @frame_writer.finish
107
+ @tmp_video.flush
108
+
109
+ # Chrome sends a frame as soon as the screencast starts, so a test that fails
110
+ # before it draws anything still leaves behind the one frame of the blank page
111
+ # it started on.
112
+ if @frame_writer.frame_count < 2
113
+ warn "[TestRecorder] The screencast captured no page updates, so no video was saved."
114
+ @tmp_video.close!
115
+ return false
116
+ end
117
+
118
+ # Each frame carries its own timestamp in the Matroska stream, so ffmpeg knows how long to
119
+ # hold it and duplicates frames to reach the constant output frame rate on its own. Without a
120
+ # known end, though, ffmpeg pads however long it likes past the last real timestamp, so -t
121
+ # caps the output at the video's real duration explicitly.
122
+ duration = @frame_writer.duration + FFMPEG_DURATION_SLACK_S
123
+ result = system("ffmpeg", "-loglevel", "error", "-f", "matroska", "-i", @tmp_video.path, "-t", duration.to_s, "-r", TestRecorder.frame_rate.to_s, *FFMPEG_ENCODE_OPTIONS, video_path)
124
+
125
+ @tmp_video.close!
126
+
127
+ if result.nil?
128
+ warn "[TestRecorder] Failed to execute ffmpeg. Please make sure that FFmpeg is installed."
129
+ return false
130
+ elsif !result
131
+ warn "[TestRecorder] ffmpeg failed to encode #{video_path}."
132
+ return false
133
+ end
134
+
135
+ true
136
+ end
137
+ end
138
+ end
@@ -1,3 +1,3 @@
1
1
  module TestRecorder
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/test_recorder.rb CHANGED
@@ -4,11 +4,12 @@ module TestRecorder
4
4
  DEFAULT_JPEG_QUALITY = 60
5
5
  DEFAULT_MAX_DIMENSION = 1000
6
6
  DEFAULT_EVERY_NTH_FRAME = 1
7
+ DEFAULT_FRAME_RATE = 25
7
8
 
8
9
  CHARS_TO_TRANSLATE = ['/', '.', ':', ',', "'", '"', " "].freeze
9
10
 
10
11
  class << self
11
- attr_writer :jpeg_quality, :max_dimension, :every_nth_frame
12
+ attr_writer :jpeg_quality, :max_dimension, :every_nth_frame, :frame_rate
12
13
 
13
14
  def enable!
14
15
  @enable = true
@@ -34,6 +35,10 @@ module TestRecorder
34
35
  defined?(@every_nth_frame) ? @every_nth_frame : DEFAULT_EVERY_NTH_FRAME
35
36
  end
36
37
 
38
+ def frame_rate
39
+ defined?(@frame_rate) ? @frame_rate : DEFAULT_FRAME_RATE
40
+ end
41
+
37
42
  def sanitize_filename(name)
38
43
  name.tr(CHARS_TO_TRANSLATE.join, "_")
39
44
  end
@@ -12,6 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
13
13
 
14
14
  spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["rubygems_mfa_required"] = "true"
15
16
 
16
17
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
17
18
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-recorder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuji Yaginuma
@@ -90,6 +90,8 @@ files:
90
90
  - lib/test_recorder/rails/setup_and_teardown.rb
91
91
  - lib/test_recorder/rspec.rb
92
92
  - lib/test_recorder/rspec/example_wrapper.rb
93
+ - lib/test_recorder/screen_recording.rb
94
+ - lib/test_recorder/screencast.rb
93
95
  - lib/test_recorder/version.rb
94
96
  - test-recorder.gemspec
95
97
  homepage: http://github.com/y-yagi/test-recorder
@@ -97,6 +99,7 @@ licenses:
97
99
  - MIT
98
100
  metadata:
99
101
  homepage_uri: http://github.com/y-yagi/test-recorder
102
+ rubygems_mfa_required: 'true'
100
103
  rdoc_options: []
101
104
  require_paths:
102
105
  - lib