mbeditor 0.13.0 → 0.13.1

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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -1
  3. data/app/assets/javascripts/mbeditor/application.js +0 -1
  4. data/app/assets/javascripts/mbeditor/collaboration_service.js +22 -2
  5. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +11 -87
  6. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
  7. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +171 -205
  8. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +35 -7
  9. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +97 -75
  10. data/app/assets/javascripts/mbeditor/editor_plugins.js +7 -1
  11. data/app/assets/javascripts/mbeditor/file_service.js +8 -18
  12. data/app/assets/javascripts/mbeditor/search_service.js +20 -2
  13. data/app/assets/javascripts/mbeditor/tab_manager.js +7 -4
  14. data/app/assets/stylesheets/mbeditor/editor.css +138 -67
  15. data/app/channels/mbeditor/collaboration_channel.rb +8 -2
  16. data/app/controllers/mbeditor/editors_controller.rb +8 -26
  17. data/app/services/mbeditor/collaboration_doc_store.rb +42 -3
  18. data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
  19. data/app/services/mbeditor/js_globals_service.rb +12 -1
  20. data/app/services/mbeditor/rubocop_run_service.rb +17 -5
  21. data/app/services/mbeditor/schema_service.rb +8 -2
  22. data/app/services/mbeditor/search_replace_service.rb +11 -2
  23. data/app/services/mbeditor/test_runner_service.rb +3 -56
  24. data/lib/mbeditor/configuration.rb +0 -8
  25. data/lib/mbeditor/route_map.rb +0 -1
  26. data/lib/mbeditor/version.rb +1 -1
  27. data/lib/tasks/mbeditor.rake +23 -0
  28. metadata +4 -3
  29. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
@@ -22,6 +22,17 @@ module Mbeditor
22
22
 
23
23
  IDENTIFIER = /[A-Za-z_$][A-Za-z0-9_$]*/
24
24
 
25
+ # Runtime globals no static scan can reach, for the same reason React and
26
+ # lodash can't be reached: a UMD bundle assigns its name through a closure
27
+ # parameter. react-rails ships `root["ReactRailsUJS"] = factory()` in the
28
+ # *gem's* lib/assets/javascripts/react_ujs.js — outside the workspace, so
29
+ # it is never scanned, and bracket-assigned to a parameter, so widening
30
+ # PATTERN to `IDENT["Str"] =` would not help and would declare thousands
31
+ # of junk names out of every minified bundle. Seeded like
32
+ # js_global_identifiers, so both consumers (the TS worker's ambient
33
+ # declarations and the babel scope lint's whitelist) get them.
34
+ KNOWN_RUNTIME_GLOBALS = %w[ReactRailsUJS].freeze
35
+
25
36
  # Minified bundles are the reason for both guards below.
26
37
  #
27
38
  # A minified file is one enormous line, and it usually opens with a
@@ -70,7 +81,7 @@ module Mbeditor
70
81
  # Seeded first so the cap can never drop them: they are the names the
71
82
  # host app declared it needs, and appending them after the scan meant
72
83
  # first(MAX_SYMBOLS) silently discarded every one on a big workspace.
73
- configured_identifiers.each do |name|
84
+ (KNOWN_RUNTIME_GLOBALS + configured_identifiers).each do |name|
74
85
  symbols[name] = { name: name, file: nil, line: nil, kind: "configured" }
75
86
  end
76
87
 
@@ -25,7 +25,7 @@ module Mbeditor
25
25
  chdir: workspace_root.to_s,
26
26
  env: { "RUBOCOP_CACHE_ROOT" => File.join(Dir.tmpdir, "rubocop") }
27
27
  )
28
- parse(result[:stdout], result[:stderr], matcher)
28
+ parse(result[:stdout], result[:stderr], matcher, mode: mode)
29
29
  rescue ProcessRunner::TimeoutError
30
30
  error("RuboCop timed out after #{timeout}s")
31
31
  rescue StandardError => e
@@ -43,7 +43,15 @@ module Mbeditor
43
43
  # ponytail: filtered after the fact, so a host in that state still pays for
44
44
  # the walk. Generate a config that inherits from theirs and re-adds the
45
45
  # excludes if the run time ever becomes the complaint.
46
- def parse(stdout, stderr = nil, matcher = nil)
46
+ #
47
+ # `-a` reports what it *fixed* alongside what it left: a corrected offense
48
+ # still appears in the JSON, with `corrected: true` and `correctable: true`.
49
+ # Rendering those made the Fix button look inert — the list after a run was
50
+ # byte-identical to the list before it, and the correctable tally never
51
+ # moved, so the button stayed lit and every further click was a slow no-op.
52
+ # They are dropped here rather than at the panel because /rubocop's response
53
+ # is documented as the post-correction offense list.
54
+ def parse(stdout, stderr = nil, matcher = nil, mode: :check)
47
55
  idx = stdout.index("{")
48
56
  return error(stderr.to_s.strip.split("\n").last || "RuboCop produced no output") unless idx
49
57
 
@@ -51,7 +59,7 @@ module Mbeditor
51
59
  files = (data["files"] || []).filter_map do |file|
52
60
  next if matcher&.excluded?(file["path"].to_s)
53
61
 
54
- offenses = file["offenses"] || []
62
+ offenses = (file["offenses"] || []).reject { |o| o["corrected"] == true }
55
63
  next if offenses.empty?
56
64
 
57
65
  { path: file["path"], offenses: offenses.map { |o| offense(o) } }
@@ -61,7 +69,12 @@ module Mbeditor
61
69
  ok: true,
62
70
  files: files.sort_by { |f| f[:path].to_s },
63
71
  summary: data["summary"] || {},
64
- correctable: files.sum { |f| f[:offenses].count { |o| o[:correctable] } }
72
+ # RuboCop's `correctable` means "a cop could rewrite this", safely or
73
+ # not. `-a` already looped to convergence, so whatever survived it needs
74
+ # `-A` — which this whole-workspace path deliberately never runs. Left
75
+ # as the raw tally, the Fix button never disabled: Style/
76
+ # FrozenStringLiteralComment and friends are correctable forever.
77
+ correctable: mode.to_sym == :autocorrect ? 0 : files.sum { |f| f[:offenses].count { |o| o[:correctable] } }
65
78
  }
66
79
  rescue JSON::ParserError => e
67
80
  error(e.message)
@@ -73,7 +86,6 @@ module Mbeditor
73
86
  message: o["message"],
74
87
  severity: o["severity"],
75
88
  correctable: o["correctable"] == true,
76
- corrected: o["corrected"] == true,
77
89
  line: o.dig("location", "start_line") || o.dig("location", "line") || 1,
78
90
  column: o.dig("location", "start_column") || o.dig("location", "column") || 1
79
91
  }
@@ -44,10 +44,16 @@ module Mbeditor
44
44
  def derive_table_name(model_name)
45
45
  normalized = model_name.delete(" ")
46
46
 
47
- # Check model file for an explicit table_name override
47
+ # Check model file for an explicit table_name override.
48
+ #
49
+ # Inflector.underscore is not a sanitizer — it leaves "../" untouched, so
50
+ # a model name reaches this join as a path fragment. Callers are expected
51
+ # to have validated the name, but the containment check is repeated here
52
+ # so the service is safe on its own; it also covers a symlink under
53
+ # app/models pointing outside the workspace, which no name check can.
48
54
  singular = ActiveSupport::Inflector.underscore(normalized)
49
55
  model_file = File.join(@workspace_root, "app", "models", "#{singular}.rb")
50
- if File.exist?(model_file)
56
+ if SafePath.within?(@workspace_root, model_file) && File.exist?(model_file)
51
57
  begin
52
58
  source = File.read(model_file, encoding: "utf-8")
53
59
  # Matches: self.table_name = "name" or = :name or = 'name'
@@ -393,7 +393,8 @@ module Mbeditor
393
393
  return {
394
394
  file: relative_path(md.dig("path", "text").to_s, root),
395
395
  line: md.dig("line_number"),
396
- text: raw_text.strip
396
+ text: raw_text.strip,
397
+ lead: leading_ws(raw_text)
397
398
  }.merge(cols)
398
399
  end
399
400
 
@@ -412,10 +413,18 @@ module Mbeditor
412
413
  end
413
414
 
414
415
  raw_text = Regexp.last_match(3)
415
- { file: file_path, line: Regexp.last_match(2).to_i, text: raw_text.strip }
416
+ { file: file_path, line: Regexp.last_match(2).to_i, text: raw_text.strip, lead: leading_ws(raw_text) }
416
417
  .merge(match_columns(raw_text, pattern))
417
418
  end
418
419
 
420
+ # How many characters `strip` took off the FRONT of the line. `col`/
421
+ # `end_col` are measured against the raw line (see match_columns), so
422
+ # without this the client cannot say where the match sits inside the
423
+ # trimmed `text` it renders — which is what highlighting needs.
424
+ def leading_ws(raw_text)
425
+ raw_text.length - raw_text.lstrip.length
426
+ end
427
+
419
428
  # 1-based Monaco columns for the first match on a hit line. Returns an
420
429
  # empty hash when there is no usable pattern or it doesn't match — the
421
430
  # row is still a valid result, it just opens at the start of the line.
@@ -10,9 +10,9 @@ module Mbeditor
10
10
  # Follows the same process-group kill pattern used by the lint endpoint to
11
11
  # enforce a configurable timeout.
12
12
  module TestRunnerService
13
- # Cap on the output shipped to the browser. A whole-suite run emits
14
- # megabytes of it, and the tail is the part that matters (the failure list
15
- # and the summary). Parsing still sees the full output.
13
+ # Cap on the output shipped to the browser. A verbose run emits megabytes
14
+ # of it, and the tail is the part that matters (the failure list and the
15
+ # summary). Parsing still sees the full output.
16
16
  MAX_RAW_BYTES = 256_000
17
17
 
18
18
  module_function
@@ -45,59 +45,6 @@ module Mbeditor
45
45
  error_result(e.message)
46
46
  end
47
47
 
48
- # Run the whole suite in +repo_path+ — no file argument, so the framework's
49
- # own default target applies (test/ for Rails, spec/ for RSpec).
50
- #
51
- # Same return shape as +run+, so the panel renders one result type. The
52
- # framework is detected from the project rather than from a filename,
53
- # since there isn't one.
54
- def run_all(repo_path, framework: nil, command: nil, timeout: 1800)
55
- framework = detect_suite_framework(repo_path) if framework.nil?
56
- return error_result("Could not detect test framework") unless framework
57
-
58
- cmd = build_suite_command(repo_path, framework, command)
59
- raw = execute_with_timeout(repo_path, cmd, timeout)
60
- tests, summary = parse_output(raw, framework, repo_path: repo_path)
61
- {
62
- ok: true,
63
- framework: framework.to_s,
64
- summary: summary,
65
- tests: tests,
66
- raw: truncate_raw(raw)
67
- }
68
- rescue ProcessRunner::TimeoutError
69
- error_result("Test run timed out after #{timeout} seconds")
70
- rescue StandardError => e
71
- error_result(e.message)
72
- end
73
-
74
- # No test_path to go on, so this reads the project layout only. RSpec wins
75
- # a tie: a project with both usually keeps `test/` for legacy fixtures.
76
- def detect_suite_framework(repo_path)
77
- return :rspec if File.exist?(File.join(repo_path, ".rspec"))
78
- return :rspec if File.directory?(File.join(repo_path, "spec"))
79
- return :minitest if File.directory?(File.join(repo_path, "test"))
80
-
81
- nil
82
- end
83
-
84
- def build_suite_command(repo_path, framework, custom_command)
85
- return Shellwords.split(custom_command) if custom_command.present?
86
-
87
- case framework.to_sym
88
- when :rspec
89
- bin = File.join(repo_path, "bin", "rspec")
90
- (File.exist?(bin) ? [bin] : ["bundle", "exec", "rspec"]) + ["--format", "json"]
91
- else
92
- bin = File.join(repo_path, "bin", "rails")
93
- # `bin/rails test` with no path runs the default suite. Without it,
94
- # `rake test` is the portable fallback for a non-Rails project.
95
- return [bin, "test", "--verbose"] if File.exist?(bin)
96
-
97
- ["bundle", "exec", "rake", "test"]
98
- end
99
- end
100
-
101
48
  # Given a source file path, resolve it to its matching test/spec file.
102
49
  # If the file is already a test/spec file, return it as-is.
103
50
  def resolve_test_file(repo_path, relative_path)
@@ -5,7 +5,6 @@ module Mbeditor
5
5
  attr_accessor :allowed_environments, :workspace_root, :excluded_paths, :rubocop_command, :rubocop_server,
6
6
  :redmine_enabled, :redmine_url, :redmine_api_key, :redmine_ticket_source,
7
7
  :test_framework, :test_command, :test_timeout,
8
- :test_all_command, :test_all_timeout,
9
8
  :authenticate_with, :cable_authenticate_with, :authentication_cache_ttl, :user_name_callback, :user_name_methods,
10
9
  :lint_timeout, :base_branch_candidates, :git_timeout, :search_timeout,
11
10
  :ruby_def_include_dirs, :related_files_custom_paths,
@@ -32,13 +31,6 @@ module Mbeditor
32
31
  # of it booting Rails before the first assertion runs, so a perfectly
33
32
  # healthy single-file run reported "timed out".
34
33
  @test_timeout = 180
35
- # A whole-suite run is a different order of magnitude — minutes, not
36
- # seconds — so it gets its own ceiling rather than forcing test_timeout
37
- # up to a value that lets one hung example block for half an hour.
38
- @test_all_timeout = 1800
39
- # Sibling of test_command, for the same reason: a project that needs a
40
- # custom runner for one file needs one for the suite too. nil auto-detects.
41
- @test_all_command = nil
42
34
  @lint_timeout = 15 # seconds for RuboCop/haml-lint subprocesses
43
35
  @base_branch_candidates = %w[origin/develop origin/main origin/master develop main master]
44
36
  @git_timeout = 10 # seconds; nil disables (no timeout on git subprocesses)
@@ -57,7 +57,6 @@ module Mbeditor
57
57
  post 'format', to: 'editors#format_file'
58
58
  post 'rubocop', to: 'editors#rubocop_run'
59
59
  post 'test', to: 'editors#run_test'
60
- post 'test_all', to: 'editors#run_all_tests'
61
60
  get 'logs/tail', to: 'logs#tail'
62
61
 
63
62
  # ── Git & Code Review ──────────────────────────────────────────────────────
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.13.0"
4
+ VERSION = "0.13.1"
5
5
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :mbeditor do
4
+ desc "Scan the workspace for files that contain a duplicated copy of themselves"
5
+ task scan_duplicates: :environment do
6
+ root = ENV["MBEDITOR_WORKSPACE_ROOT"] || Mbeditor::WorkspaceRootResolver.call
7
+ findings = Mbeditor::DuplicateContentScanner.new(root).call
8
+
9
+ if findings.empty?
10
+ puts "No duplicated files found under #{root}."
11
+ next
12
+ end
13
+
14
+ puts "Possible duplicated content under #{root}:"
15
+ findings.each do |f|
16
+ label = f.reason == :exact ? "DUPLICATED (whole file appears twice)" : "suspect (opening block repeats)"
17
+ puts format(" %-60s %s at line %d of %d", f.path, label, f.line, f.lines)
18
+ end
19
+ puts
20
+ puts "Files marked DUPLICATED are byte-for-byte X+X — check `git diff` and delete the second half."
21
+ puts "Files marked suspect need a look; the repeat may be legitimate."
22
+ end
23
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbeditor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-20 00:00:00.000000000 Z
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -80,7 +80,6 @@ files:
80
80
  - app/assets/javascripts/mbeditor/components/ShortcutHelp.js
81
81
  - app/assets/javascripts/mbeditor/components/TabBar.js
82
82
  - app/assets/javascripts/mbeditor/components/TestResultsPanel.js
83
- - app/assets/javascripts/mbeditor/components/TestRunPanel.js
84
83
  - app/assets/javascripts/mbeditor/conflict_parser.js
85
84
  - app/assets/javascripts/mbeditor/editor_plugins.js
86
85
  - app/assets/javascripts/mbeditor/editor_store.js
@@ -108,6 +107,7 @@ files:
108
107
  - app/services/mbeditor/availability_probe.rb
109
108
  - app/services/mbeditor/code_search_service.rb
110
109
  - app/services/mbeditor/collaboration_doc_store.rb
110
+ - app/services/mbeditor/duplicate_content_scanner.rb
111
111
  - app/services/mbeditor/editor_state_service.rb
112
112
  - app/services/mbeditor/exclusion_matcher.rb
113
113
  - app/services/mbeditor/file_import_service.rb
@@ -163,6 +163,7 @@ files:
163
163
  - lib/mbeditor/route_map.rb
164
164
  - lib/mbeditor/ruby_lsp_client.rb
165
165
  - lib/mbeditor/version.rb
166
+ - lib/tasks/mbeditor.rake
166
167
  - mbeditor.gemspec
167
168
  - public/mbeditor-icon.svg
168
169
  - public/monaco-editor/VERSIONS
@@ -1,312 +0,0 @@
1
- 'use strict';
2
-
3
- // TestRunPanel — bottom drawer for a whole-suite run.
4
- //
5
- // Deliberately not a modal, unlike the per-file TestResultsPanel: a suite run
6
- // takes minutes, and a backdrop that blocks the editor for the duration turns
7
- // the wait into dead time. This docks alongside Problems and the log, so you
8
- // can keep reading code while it runs and keep the failures on screen while
9
- // you fix them.
10
- //
11
- // Structure and CSS are shared with ProblemsPanel on purpose — same drawer,
12
- // same header, same clickable rows — rather than a second near-identical
13
- // stylesheet.
14
- var TestRunPanel = (function () {
15
- var STATUS_ICON = {
16
- pass: 'fa-check-circle',
17
- fail: 'fa-times-circle',
18
- error: 'fa-exclamation-circle',
19
- skip: 'fa-forward'
20
- };
21
- var STATUS_KIND = { fail: 'error', error: 'error', skip: 'info', pass: 'info' };
22
-
23
- function isFailure(t) { return t.status === 'fail' || t.status === 'error'; }
24
-
25
- // Closing the drawer unmounts it, so the result is kept outside the
26
- // component. localStorage rather than a module variable, to match the
27
- // per-file test cache in MbeditorApp — a suite run is the most expensive
28
- // thing the editor can ask for, and losing it to a page reload is the same
29
- // annoyance as losing it to a close.
30
- var CACHE_KEY = 'mbeditor_test_result_suite';
31
- // The runner's raw output is unbounded — a verbose suite easily runs to
32
- // megabytes, and a quota error means nothing is cached at all, which is the
33
- // exact failure this is here to prevent. The tail is the part worth keeping:
34
- // failures and the summary line are at the end.
35
- var RAW_CACHE_LIMIT = 200000;
36
-
37
- function loadCached() {
38
- try {
39
- var stored = window.localStorage.getItem(CACHE_KEY);
40
- return stored ? JSON.parse(stored) : null;
41
- } catch (e) {
42
- return null;
43
- }
44
- }
45
-
46
- function saveCached(result) {
47
- try {
48
- var raw = result && result.raw;
49
- var slim = (raw && raw.length > RAW_CACHE_LIMIT)
50
- ? Object.assign({}, result, { raw: '…output truncated…\n' + raw.slice(-RAW_CACHE_LIMIT) })
51
- : result;
52
- window.localStorage.setItem(CACHE_KEY, JSON.stringify(slim));
53
- } catch (e) { /* quota or storage blocked — the run still shows, just isn't kept */ }
54
- }
55
-
56
- function clearCached() {
57
- try { window.localStorage.removeItem(CACHE_KEY); } catch (e) {}
58
- }
59
-
60
- function ranAtLabel(result) {
61
- if (!result || !result.cachedAt) return null;
62
- try {
63
- return 'ran ' + new Date(result.cachedAt).toLocaleTimeString();
64
- } catch (e) {
65
- return null;
66
- }
67
- }
68
-
69
- // Failures grouped by the file they live in — the unit you actually open.
70
- // Anything the server could not resolve to a workspace-relative path is
71
- // still listed, just not openable.
72
- function byFile(tests) {
73
- var order = [];
74
- var groups = {};
75
- tests.filter(isFailure).forEach(function (t) {
76
- var key = t.file || '';
77
- if (!groups[key]) { groups[key] = []; order.push(key); }
78
- groups[key].push(t);
79
- });
80
- return order.map(function (k) { return { path: k, tests: groups[k] }; });
81
- }
82
-
83
- var Panel = function TestRunPanelComponent(_ref) {
84
- var onClose = _ref.onClose;
85
- var onOpenFile = _ref.onOpenFile;
86
- // Hands the result to the app so the editors can draw the same inline
87
- // pass/fail zones a per-file run draws. Without it, opening a failing file
88
- // from this drawer showed a bare editor.
89
- var onResult = _ref.onResult;
90
-
91
- var _result = React.useState(loadCached);
92
- var result = _result[0], setResult = _result[1];
93
-
94
- // Kept in a ref so the publish effect below can fire on mount without
95
- // listing onResult (a fresh closure every render) as a dependency.
96
- var onResultRef = React.useRef(onResult);
97
- onResultRef.current = onResult;
98
-
99
- // Publish whatever the drawer is showing, including a result restored from
100
- // the cache on mount — reopening the drawer should put the decorations
101
- // back, not just the list.
102
- React.useEffect(function () {
103
- if (onResultRef.current) onResultRef.current(result);
104
- }, [result]);
105
- var _running = React.useState(false);
106
- var running = _running[0], setRunning = _running[1];
107
- var _showRaw = React.useState(false);
108
- var showRaw = _showRaw[0], setShowRaw = _showRaw[1];
109
-
110
- // Elapsed seconds while a run is in flight. A suite run is long enough
111
- // that a spinner with no number reads as "hung".
112
- var _elapsed = React.useState(0);
113
- var elapsed = _elapsed[0], setElapsed = _elapsed[1];
114
- React.useEffect(function () {
115
- if (!running) return;
116
- var started = Date.now();
117
- var id = setInterval(function () {
118
- setElapsed(Math.round((Date.now() - started) / 1000));
119
- }, 1000);
120
- return function () { clearInterval(id); };
121
- }, [running]);
122
-
123
- var run = function () {
124
- if (running) return;
125
- setRunning(true);
126
- setElapsed(0);
127
- var finish = function (data) {
128
- var stamped = Object.assign({}, data, { cachedAt: Date.now() });
129
- setResult(stamped);
130
- saveCached(stamped);
131
- };
132
- FileService.runAllTests()
133
- .then(finish)
134
- ["catch"](function (e) {
135
- var res = e && e.response && e.response.data;
136
- finish(res || { ok: false, error: (e && e.message) || 'Test run failed', tests: [] });
137
- })
138
- .then(function () { setRunning(false); });
139
- };
140
-
141
- var failures = byFile((result && result.tests) || []);
142
- var openable = failures.filter(function (g) { return g.path; });
143
-
144
- var openFailing = function () {
145
- if (!onOpenFile) return;
146
- openable.forEach(function (g) {
147
- onOpenFile(g.path, (g.tests[0] && g.tests[0].line) || 1, 1);
148
- });
149
- };
150
-
151
- // A checkout changes what the suite would even run, so the old result is
152
- // dropped rather than left looking current — same rule as the RuboCop
153
- // snapshot in the Problems panel.
154
- React.useEffect(function () {
155
- var onBranchChanged = function () { setResult(null); clearCached(); };
156
- window.addEventListener('mbeditor:branch-changed', onBranchChanged);
157
- return function () { window.removeEventListener('mbeditor:branch-changed', onBranchChanged); };
158
- }, []);
159
-
160
- var MIN_HEIGHT = 120;
161
- var _height = React.useState(function () {
162
- var saved = parseInt(window.localStorage.getItem('mbeditorTestRunHeight'), 10);
163
- return (saved && saved >= MIN_HEIGHT) ? saved : 260;
164
- });
165
- var height = _height[0], setHeight = _height[1];
166
- var heightRef = React.useRef(height);
167
- heightRef.current = height;
168
-
169
- var onResizeMouseDown = function (e) {
170
- e.preventDefault();
171
- var startY = e.clientY;
172
- var startHeight = heightRef.current;
173
- var onMove = function (ev) {
174
- var vh = window.innerHeight || document.documentElement.clientHeight || 0;
175
- var maxH = vh > 0 ? Math.round(vh * 0.85) : Infinity;
176
- setHeight(Math.min(maxH, Math.max(MIN_HEIGHT, startHeight + (startY - ev.clientY))));
177
- };
178
- var onUp = function () {
179
- document.removeEventListener('mousemove', onMove);
180
- document.removeEventListener('mouseup', onUp);
181
- window.localStorage.setItem('mbeditorTestRunHeight', String(heightRef.current));
182
- };
183
- document.addEventListener('mousemove', onMove);
184
- document.addEventListener('mouseup', onUp);
185
- };
186
-
187
- var summary = result && result.summary;
188
- var failCount = failures.reduce(function (n, g) { return n + g.tests.length; }, 0);
189
-
190
- return React.createElement(
191
- 'div',
192
- { className: 'ide-problems-drawer ide-testrun-drawer', style: { height: height + 'px' } },
193
- React.createElement('div', {
194
- className: 'ide-problems-resize', title: 'Drag to resize', onMouseDown: onResizeMouseDown
195
- }),
196
- React.createElement(
197
- 'div',
198
- { className: 'ide-problems-header' },
199
- React.createElement('i', { className: 'fas fa-flask' }),
200
- React.createElement('span', { className: 'ide-problems-title' }, 'Test Run'),
201
- summary && React.createElement(
202
- 'div',
203
- { className: 'ide-problems-severity-filter' },
204
- React.createElement('span', { className: 'ide-testrun-stat ide-testrun-pass' },
205
- (summary.passed || 0) + ' passed'),
206
- React.createElement('span', { className: 'ide-testrun-stat ide-testrun-fail' },
207
- ((summary.failed || 0) + (summary.errored || 0)) + ' failed'),
208
- (summary.skipped || 0) > 0 && React.createElement('span', { className: 'ide-testrun-stat' },
209
- summary.skipped + ' skipped'),
210
- summary.duration != null && React.createElement('span', { className: 'ide-testrun-stat' },
211
- summary.duration + 's'),
212
- ranAtLabel(result) && React.createElement('span', { className: 'ide-testrun-stat' },
213
- ranAtLabel(result))
214
- ),
215
- React.createElement(
216
- 'div',
217
- { className: 'ide-problems-actions' },
218
- React.createElement('button', {
219
- type: 'button', className: 'ide-problems-btn',
220
- disabled: running,
221
- title: 'Run the whole test suite',
222
- onClick: run
223
- },
224
- React.createElement('i', { className: running ? 'fas fa-spinner fa-spin' : 'fas fa-play' }),
225
- React.createElement('span', null, running ? ' Running ' + elapsed + 's' : ' Run all')),
226
- React.createElement('button', {
227
- type: 'button', className: 'ide-problems-btn',
228
- disabled: openable.length === 0,
229
- title: openable.length
230
- ? 'Open all ' + openable.length + ' file(s) with failures'
231
- : 'No failing files to open',
232
- onClick: openFailing
233
- },
234
- React.createElement('i', { className: 'fas fa-folder-open' }),
235
- React.createElement('span', null, ' Open failing' + (openable.length ? ' (' + openable.length + ')' : ''))),
236
- result && result.raw && React.createElement('button', {
237
- type: 'button',
238
- className: 'ide-problems-btn' + (showRaw ? ' is-on' : ''),
239
- title: 'Show the runner’s raw output',
240
- 'aria-pressed': showRaw ? 'true' : 'false',
241
- onClick: function () { setShowRaw(function (p) { return !p; }); }
242
- },
243
- React.createElement('i', { className: 'fas fa-terminal' }),
244
- React.createElement('span', null, ' Output'))
245
- ),
246
- React.createElement('button', {
247
- type: 'button', className: 'ide-problems-btn',
248
- title: 'Close', onClick: onClose
249
- }, React.createElement('i', { className: 'fas fa-times' }))
250
- ),
251
- React.createElement(
252
- 'div',
253
- { className: 'ide-problems-body' },
254
- result && result.error && React.createElement('div',
255
- { className: 'ide-problems-empty' }, 'Test run failed: ' + result.error),
256
- showRaw && result && result.raw
257
- ? React.createElement('pre', { className: 'ide-testrun-raw' }, result.raw)
258
- : !result && !running
259
- ? React.createElement('div', { className: 'ide-problems-empty' },
260
- 'Press Run all to run the whole suite')
261
- : running && !result
262
- ? React.createElement('div', { className: 'ide-problems-empty' },
263
- 'Running the suite… ' + elapsed + 's')
264
- : failCount === 0 && result && !result.error
265
- ? React.createElement('div', { className: 'ide-problems-empty' }, 'No failing tests')
266
- : failures.map(function (group) {
267
- return React.createElement(
268
- 'div',
269
- { className: 'ide-problems-file', key: 'tr:' + (group.path || '(unknown)') },
270
- React.createElement(
271
- 'div',
272
- { className: 'ide-problems-file-name' },
273
- React.createElement('i', { className: 'fas fa-flask', 'aria-hidden': 'true' }),
274
- ' ' + (group.path || 'Unknown file'),
275
- React.createElement('span', { className: 'ide-problems-file-count' }, group.tests.length)
276
- ),
277
- group.tests.map(function (t, i) {
278
- var kind = STATUS_KIND[t.status] || 'info';
279
- return React.createElement(
280
- 'button',
281
- {
282
- type: 'button',
283
- className: 'ide-problems-item ide-problems-item-' + kind,
284
- key: 'tr:' + (group.path || '') + ':' + i,
285
- disabled: !group.path,
286
- title: t.name + (t.message ? '\n' + t.message : ''),
287
- 'aria-label': t.status + ': ' + t.name +
288
- (group.path ? ', ' + group.path + ' line ' + (t.line || 1) : ''),
289
- onClick: function () {
290
- if (group.path && onOpenFile) onOpenFile(group.path, t.line || 1, 1);
291
- }
292
- },
293
- React.createElement('i', {
294
- className: 'fas ' + (STATUS_ICON[t.status] || 'fa-circle') + ' ide-problems-icon',
295
- 'aria-hidden': 'true'
296
- }),
297
- React.createElement('span', { className: 'ide-problems-msg' }, t.name),
298
- t.message && React.createElement('code', { className: 'ide-problems-code' },
299
- t.message.split('\n')[0]),
300
- t.line && React.createElement('span', { className: 'ide-problems-loc' }, '[' + t.line + ']')
301
- );
302
- })
303
- );
304
- })
305
- )
306
- );
307
- };
308
-
309
- return Panel;
310
- })();
311
-
312
- window.TestRunPanel = TestRunPanel;