diff options
-rw-r--r-- | app/assets/javascripts/jobs/components/job_log.vue | 53 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/builds.scss | 4 | ||||
-rw-r--r-- | changelogs/unreleased/generate-spans-for-sections.yml | 5 | ||||
-rw-r--r-- | lib/gitlab/ci/ansi2html.rb | 87 | ||||
-rw-r--r-- | spec/controllers/projects/jobs_controller_spec.rb | 2 | ||||
-rw-r--r-- | spec/factories/ci/builds.rb | 20 | ||||
-rw-r--r-- | spec/features/projects/jobs/user_browses_job_spec.rb | 46 | ||||
-rw-r--r-- | spec/fixtures/trace/trace_with_duplicate_sections | 30 | ||||
-rw-r--r-- | spec/javascripts/jobs/components/job_log_spec.js | 37 | ||||
-rw-r--r-- | spec/javascripts/jobs/mock_data.js | 15 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/ansi2html_spec.rb | 62 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/trace/stream_spec.rb | 15 | ||||
-rw-r--r-- | spec/support/shared_examples/ci_trace_shared_examples.rb | 4 |
13 files changed, 323 insertions, 57 deletions
diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue index 92e20e92d66..d611b370ab9 100644 --- a/app/assets/javascripts/jobs/components/job_log.vue +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -17,10 +17,19 @@ export default { ...mapState(['isScrolledToBottomBeforeReceivingTrace']), }, updated() { - this.$nextTick(() => this.handleScrollDown()); + this.$nextTick(() => { + this.handleScrollDown(); + this.handleCollapsibleRows(); + }); }, mounted() { - this.$nextTick(() => this.handleScrollDown()); + this.$nextTick(() => { + this.handleScrollDown(); + this.handleCollapsibleRows(); + }); + }, + destroyed() { + this.removeEventListener(); }, methods: { ...mapActions(['scrollBottom']), @@ -38,21 +47,45 @@ export default { }, 0); } }, + removeEventListener() { + this.$el + .querySelectorAll('.js-section-start') + .forEach(el => el.removeEventListener('click', this.handleSectionClick)); + }, + /** + * The collapsible rows are sent in HTML from the backend + * We need tos add a onclick handler for the divs that match `.js-section-start` + * + */ + handleCollapsibleRows() { + this.$el + .querySelectorAll('.js-section-start') + .forEach(el => el.addEventListener('click', this.handleSectionClick)); + }, + /** + * On click, we toggle the hidden class of + * all the rows that match the `data-section` selector + */ + handleSectionClick(evt) { + const clickedArrow = evt.currentTarget; + // toggle the arrow class + clickedArrow.classList.toggle('fa-caret-right'); + clickedArrow.classList.toggle('fa-caret-down'); + + const { section } = clickedArrow.dataset; + const sibilings = this.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`); + + sibilings.forEach(row => row.classList.toggle('hidden')); + }, }, }; </script> <template> <pre class="js-build-trace build-trace qa-build-trace"> - <code - class="bash" - v-html="trace" - > + <code class="bash" v-html="trace"> </code> - <div - v-if="!isComplete" - class="js-log-animation build-loader-animation" - > + <div v-if="!isComplete" class="js-log-animation build-loader-animation"> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 6fc742871e7..6e98908eeed 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -124,6 +124,10 @@ float: left; padding-left: $gl-padding-8; } + + .section-header ~ .section.line { + margin-left: $gl-padding; + } } .build-header { diff --git a/changelogs/unreleased/generate-spans-for-sections.yml b/changelogs/unreleased/generate-spans-for-sections.yml new file mode 100644 index 00000000000..e167d66490f --- /dev/null +++ b/changelogs/unreleased/generate-spans-for-sections.yml @@ -0,0 +1,5 @@ +--- +title: Adds collapsible sections for job log +merge_request: 28642 +author: +type: added diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb index fba0de20ced..6109b45ffd2 100644 --- a/lib/gitlab/ci/ansi2html.rb +++ b/lib/gitlab/ci/ansi2html.rb @@ -131,9 +131,9 @@ module Gitlab def on_109(_) set_bg_color(9, 'l') end - attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask + attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask, :sections, :lineno_in_section - STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze + STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask, :sections, :lineno_in_section].freeze def convert(stream, new_state) reset_state @@ -153,10 +153,9 @@ module Gitlab start_offset = @offset - open_new_tag - stream.each_line do |line| s = StringScanner.new(line) + until s.eos? if s.scan(Gitlab::Regex.build_trace_section_regex) @@ -166,11 +165,11 @@ module Gitlab elsif s.scan(/\e(([@-_])(.*?)?)?$/) break elsif s.scan(/</) - @out << '<' + write_in_tag '<' elsif s.scan(/\r?\n/) - @out << '<br>' + handle_new_line else - @out << s.scan(/./m) + write_in_tag s.scan(/./m) end @offset += s.matched_size @@ -190,13 +189,59 @@ module Gitlab ) end + def section_to_class_name(section) + section.to_s.downcase.gsub(/[^a-z0-9]/, '-') + end + + def handle_new_line + css_classes = [] + + if @sections.any? + css_classes = %w[section line] + sections.map { |section| "s_#{section}" } + end + + write_in_tag %{<br/>} + write_raw %{<span class="#{css_classes.join(' ')}"></span>} if css_classes.any? + @lineno_in_section += 1 + open_new_tag + end + def handle_section(scanner) action = scanner[1] timestamp = scanner[2] section = scanner[3] - line = scanner.matched[0...-5] # strips \r\033[0K - @out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>} + normalized_section = section_to_class_name(section) + + if action == "start" + handle_section_start(normalized_section, timestamp) + elsif action == "end" + handle_section_end(normalized_section, timestamp) + end + end + + def handle_section_start(section, timestamp) + return if @sections.include?(section) + + @sections << section + write_raw %{<div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="#{timestamp}" data-section="#{data_section_names}" role="button"></div>} + @lineno_in_section = 0 + end + + def handle_section_end(section, timestamp) + return unless @sections.include?(section) + + # close all sections up to section + until @sections.empty? + write_raw %{<div class="section-end" data-section="#{data_section_names}"></div>} + + last_section = @sections.pop + break if section == last_section + end + end + + def data_section_names + @sections.join(" ") end def handle_sequence(scanner) @@ -217,8 +262,6 @@ module Gitlab end evaluate_command_stack(commands) - - open_new_tag end def evaluate_command_stack(stack) @@ -231,6 +274,20 @@ module Gitlab evaluate_command_stack(stack) end + def write_in_tag(data) + ensure_open_new_tag + @out << data + end + + def write_raw(data) + close_open_tags + @out << data + end + + def ensure_open_new_tag + open_new_tag if @n_open_tags == 0 + end + def open_new_tag css_classes = [] @@ -251,7 +308,11 @@ module Gitlab css_classes << "term-#{css_class}" if @style_mask & flag != 0 end - return if css_classes.empty? + if @sections.any? + css_classes << "section" + css_classes << "js-section-header" if @lineno_in_section == 0 + css_classes += sections.map { |section| "js-s-#{section}" } + end @out << %{<span class="#{css_classes.join(' ')}">} @n_open_tags += 1 @@ -268,6 +329,8 @@ module Gitlab @offset = 0 @n_open_tags = 0 @out = +'' + @sections = [] + @lineno_in_section = 0 reset end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 490e9841492..0dabe27977a 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -540,7 +540,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq job.id expect(json_response['status']).to eq job.status - expect(json_response['html']).to eq('BUILD TRACE') + expect(json_response['html']).to eq('<span class="">BUILD TRACE</span>') end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index a473136b57b..5f7c75a3a92 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -228,6 +228,26 @@ FactoryBot.define do end end + trait :trace_with_duplicate_sections do + after(:create) do |build, evaluator| + trace = File.binread( + File.expand_path( + Rails.root.join('spec/fixtures/trace/trace_with_duplicate_sections'))) + + build.trace.set(trace) + end + end + + trait :trace_with_sections do + after(:create) do |build, evaluator| + trace = File.binread( + File.expand_path( + Rails.root.join('spec/fixtures/trace/trace_with_sections'))) + + build.trace.set(trace) + end + end + trait :unicode_trace_live do after(:create) do |build, evaluator| trace = File.binread( diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index 54b462da87a..fbe765d4c44 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -34,6 +34,52 @@ describe 'User browses a job', :js do expect(page).to have_content('Job has been erased') end + shared_examples 'has collapsible sections' do + it 'collapses the section clicked' do + wait_for_requests + text_to_hide = "Cloning into '/nolith/ci-tests'" + text_to_show = 'Waiting for pod' + + expect(page).to have_content(text_to_hide) + expect(page).to have_content(text_to_show) + + first('.js-section-start[data-section="get-sources"]').click + + expect(page).not_to have_content(text_to_hide) + expect(page).to have_content(text_to_show) + end + end + + context 'when job trace contains sections' do + let!(:build) { create(:ci_build, :success, :trace_with_sections, :coverage, pipeline: pipeline) } + + it_behaves_like 'has collapsible sections' + end + + context 'when job trace contains duplicate sections' do + let!(:build) { create(:ci_build, :success, :trace_with_duplicate_sections, :coverage, pipeline: pipeline) } + + it_behaves_like 'has collapsible sections' + end + + context 'when job trace contains sections' do + let!(:build) { create(:ci_build, :success, :trace_with_duplicate_sections, :coverage, pipeline: pipeline) } + + it 'collapses a section' do + wait_for_requests + text_to_hide = "Cloning into '/nolith/ci-tests'" + text_to_show = 'Waiting for pod' + + expect(page).to have_content(text_to_hide) + expect(page).to have_content(text_to_show) + + first('.js-section-start[data-section="get-sources"]').click + + expect(page).not_to have_content(text_to_hide) + expect(page).to have_content(text_to_show) + end + end + context 'with a failed job' do let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) } diff --git a/spec/fixtures/trace/trace_with_duplicate_sections b/spec/fixtures/trace/trace_with_duplicate_sections new file mode 100644 index 00000000000..7a894e80e38 --- /dev/null +++ b/spec/fixtures/trace/trace_with_duplicate_sections @@ -0,0 +1,30 @@ +[0KRunning with gitlab-runner dev (HEAD) + on kitsune minikube (a21b584f) +[0;m[0;33mWARNING: Namespace is empty, therefore assuming 'default'. +[0;m[0KUsing Kubernetes namespace: default +[0;m[0KUsing Kubernetes executor with image alpine:3.4 ... +[0;msection_start:1506004954:prepare_script
[0KWaiting for pod default/runner-a21b584f-project-1208199-concurrent-0sg03f to be running, status is Pending +Running on runner-a21b584f-project-1208199-concurrent-0sg03f via kitsune.local... +section_end:1506004957:prepare_script
[0Ksection_start:1506004957:get_sources
[0K[32;1mCloning repository...[0;m +Cloning into '/nolith/ci-tests'... +[32;1mChecking out dddd7a6e as master...[0;m +[32;1mSkipping Git submodules setup[0;m +section_end:1506004958:get_sources
[0Ksection_start:1506004958:restore_cache
[0Ksection_end:1506004958:restore_cache
[0Ksection_start:1506004958:download_artifacts
[0Ksection_end:1506004958:download_artifacts
[0Ksection_start:1506004958:build_script
[0K[32;1m$ whoami[0;m +root +section_end:1506004959:build_script
[0Ksection_start:1506004959:after_script
[0Ksection_end:1506004959:after_script
[0Ksection_start:1506004959:archive_cache
[0Ksection_end:1506004959:archive_cache
[0Ksection_start:1506004959:upload_artifacts
[0Ksection_end:1506004959:upload_artifacts
[0K[32;1mJob succeeded +[0;m +[0KRunning with gitlab-runner dev (HEAD) + on kitsune minikube (a21b584f) +[0;m[0;33mWARNING: Namespace is empty, therefore assuming 'default'. +[0;m[0KUsing Kubernetes namespace: default +[0;m[0KUsing Kubernetes executor with image alpine:3.4 ... +[0;msection_start:1506004954:prepare_script
[0KWaiting for pod default/runner-a21b584f-project-1208199-concurrent-0sg03f to be running, status is Pending +Running on runner-a21b584f-project-1208199-concurrent-0sg03f via kitsune.local... +section_end:1506004957:prepare_script
[0Ksection_start:1506004957:get_sources
[0K[32;1mCloning repository...[0;m +Cloning into '/nolith/ci-tests'... +[32;1mChecking out dddd7a6e as master...[0;m +[32;1mSkipping Git submodules setup[0;m +section_end:1506004958:get_sources
[0Ksection_start:1506004958:restore_cache
[0Ksection_end:1506004958:restore_cache
[0Ksection_start:1506004958:download_artifacts
[0Ksection_end:1506004958:download_artifacts
[0Ksection_start:1506004958:build_script
[0K[32;1m$ whoami[0;m +root +section_end:1506004959:build_script
[0Ksection_start:1506004959:after_script
[0Ksection_end:1506004959:after_script
[0Ksection_start:1506004959:archive_cache
[0Ksection_end:1506004959:archive_cache
[0Ksection_start:1506004959:upload_artifacts
[0Ksection_end:1506004959:upload_artifacts
[0K[32;1mJob succeeded +[0;m diff --git a/spec/javascripts/jobs/components/job_log_spec.js b/spec/javascripts/jobs/components/job_log_spec.js index dc0f77ceb80..7e2ec2ec3f7 100644 --- a/spec/javascripts/jobs/components/job_log_spec.js +++ b/spec/javascripts/jobs/components/job_log_spec.js @@ -3,6 +3,7 @@ import component from '~/jobs/components/job_log.vue'; import createStore from '~/jobs/store'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../store/helpers'; +import { logWithCollapsibleSections } from '../mock_data'; describe('Job Log', () => { const Component = Vue.extend(component); @@ -62,4 +63,40 @@ describe('Job Log', () => { expect(vm.$el.querySelector('.js-log-animation')).toBeNull(); }); }); + + describe('Collapsible sections', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + props: { + trace: logWithCollapsibleSections.html, + isComplete: true, + }, + store, + }); + }); + + it('renders open arrow', () => { + expect(vm.$el.querySelector('.fa-caret-down')).not.toBeNull(); + }); + + it('toggles hidden class to the sibilings rows when arrow is clicked', done => { + vm.$nextTick() + .then(() => { + const { section } = vm.$el.querySelector('.js-section-start').dataset; + vm.$el.querySelector('.js-section-start').click(); + + vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => { + expect(el.classList.contains('hidden')).toEqual(true); + }); + + vm.$el.querySelector('.js-section-start').click(); + + vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => { + expect(el.classList.contains('hidden')).toEqual(false); + }); + }) + .then(done) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 3d40e94d219..c5022d3e93d 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -1189,3 +1189,18 @@ export const jobsInStage = { path: '/gitlab-org/gitlab-shell/pipelines/27#build', dropdown_path: '/gitlab-org/gitlab-shell/pipelines/27/stage.json?stage=build', }; + +export const logWithCollapsibleSections = { + append: false, + complete: true, + html: + '<div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="1559571405" data-section="after-script" role="button"></div><span class="term-fg-l-green term-bold section js-section-header js-s-after-script">Running after script...</span><span class="section js-section-header js-s-after-script"><br /></span><span class="section s_after-script line"></span><span class="section js-s-after-script"></span><span class="term-fg-l-green term-bold section js-s-after-script">$ date</span><span class="section js-s-after-script"><br /></span><span class="section s_after-script line"></span><span class="section js-s-after-script">Mon Jun 3 14:16:46 UTC 2019<br /></span><span class="section s_after-script line"></span><span class="section js-s-after-script"></span><div class="section-end" data-section="after-script"></div><div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer"data-timestamp="1559571408" data-section="archive-cache" role="button" ></div><span class="term-fg-l-green term-bold section js-section-header js-s-archive-cache">Not uploading cache debian-stretch-ruby-2.6.3-node-10.x-3 due to policy</span><span class="section js-section-header js-s-archive-cache"><br /></span><span class="section s_archive-cache line"></span><span class="section js-s-archive-cache"></span><div class="section-end" data-section="archive-cache"></div><div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="1559571409" data-section="upload-artifacts-on-success" role="button"></div><span class="term-fg-l-green term-bold section js-section-header js-s-upload-artifacts-on-success">Uploading artifacts...</span><span class="section js-section-header js-s-upload-artifacts-on-success"><br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">coverage/: found 5 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">knapsack/: found 4 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">rspec_flaky/: found 4 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">rspec_profiling/: found 1 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success"></span><span class="term-fg-yellow section js-s-upload-artifacts-on-success">WARNING: tmp/capybara/: no matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">Uploading artifacts to coordinator... ok </span><span class="section js-s-upload-artifacts-on-success"> id</span><span class="section js-s-upload-artifacts-on-success">=224162288 responseStatus</span><span class="section js-s-upload-artifacts-on-success">=201 Created token</span><span class="section js-s-upload-artifacts-on-success">=bBmyXJNW<br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success"></span><span class="term-fg-l-green term-bold section js-s-upload-artifacts-on-success">Uploading artifacts...</span><span class="section js-s-upload-artifacts-on-success"><br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">junit_rspec.xml: found 1 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">Uploading artifacts to coordinator... ok </span><span class="section js-s-upload-artifacts-on-success"> id</span><span class="section js-s-upload-artifacts-on-success">=224162288 responseStatus</span><span class="section js-s-upload-artifacts-on-success">=201 Created token</span><span class="section js-s-upload-artifacts-on-success">=bBmyXJNW<br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success"></span><div class="section-end" data-section="upload-artifacts-on-success"></div><span class="term-fg-l-green term-bold">Job succeeded<br /><span class="term-fg-l-green term-bold"></span></span>', + id: 1385, + offset: 0, + size: 78815, + state: + 'eyJvZmZzZXQiOjc4ODE1LCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowLCJzZWN0aW9ucyI6W10sImxpbmVub19pbl9zZWN0aW9uIjoxMX0=', + status: 'success', + total: 78815, + truncated: false, +}; diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index 5a5c071c639..ac4612dda92 100644 --- a/spec/lib/gitlab/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -4,11 +4,11 @@ describe Gitlab::Ci::Ansi2html do subject { described_class } it "prints non-ansi as-is" do - expect(convert_html("Hello")).to eq('Hello') + expect(convert_html("Hello")).to eq('<span class="">Hello</span>') end it "strips non-color-changing control sequences" do - expect(convert_html("Hello \e[2Kworld")).to eq('Hello world') + expect(convert_html("Hello \e[2Kworld")).to eq('<span class="">Hello world</span>') end it "prints simply red" do @@ -32,7 +32,7 @@ describe Gitlab::Ci::Ansi2html do end it "resets colors after red on blue" do - expect(convert_html("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world') + expect(convert_html("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span><span class=""> world</span>') end it "performs color change from red/blue to yellow/blue" do @@ -44,11 +44,11 @@ describe Gitlab::Ci::Ansi2html do end it "performs color change from red/blue to reset to yellow/green" do - expect(convert_html("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>') + expect(convert_html("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span><span class=""> </span><span class="term-fg-yellow term-bg-green">world</span>') end it "ignores unsupported codes" do - expect(convert_html("\e[51mHello\e[0m")).to eq('Hello') + expect(convert_html("\e[51mHello\e[0m")).to eq('<span class="">Hello</span>') end it "prints light red" do @@ -72,8 +72,8 @@ describe Gitlab::Ci::Ansi2html do end it "resets bold text" do - expect(convert_html("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world') - expect(convert_html("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world') + expect(convert_html("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span><span class=""> world</span>') + expect(convert_html("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span><span class=""> world</span>') end it "prints italic text" do @@ -81,7 +81,7 @@ describe Gitlab::Ci::Ansi2html do end it "resets italic text" do - expect(convert_html("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world') + expect(convert_html("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span><span class=""> world</span>') end it "prints underlined text" do @@ -89,7 +89,7 @@ describe Gitlab::Ci::Ansi2html do end it "resets underlined text" do - expect(convert_html("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world') + expect(convert_html("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span><span class=""> world</span>') end it "prints concealed text" do @@ -97,7 +97,7 @@ describe Gitlab::Ci::Ansi2html do end it "resets concealed text" do - expect(convert_html("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world') + expect(convert_html("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span><span class=""> world</span>') end it "prints crossed-out text" do @@ -105,7 +105,7 @@ describe Gitlab::Ci::Ansi2html do end it "resets crossed-out text" do - expect(convert_html("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world') + expect(convert_html("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span><span class=""> world</span>') end it "can print 256 xterm fg colors" do @@ -137,15 +137,15 @@ describe Gitlab::Ci::Ansi2html do end it "prints <" do - expect(convert_html("<")).to eq('<') + expect(convert_html("<")).to eq('<span class=""><</span>') end it "replaces newlines with line break tags" do - expect(convert_html("\n")).to eq('<br>') + expect(convert_html("\n")).to eq('<span class=""><br/><span class=""></span></span>') end it "groups carriage returns with newlines" do - expect(convert_html("\r\n")).to eq('<br>') + expect(convert_html("\r\n")).to eq('<span class=""><br/><span class=""></span></span>') end describe "incremental update" do @@ -166,14 +166,14 @@ describe Gitlab::Ci::Ansi2html do let(:pre_text) { "\e[1mHello" } let(:pre_html) { "<span class=\"term-bold\">Hello</span>" } let(:text) { "\e[1mWorld" } - let(:html) { "<span class=\"term-bold\"></span><span class=\"term-bold\">World</span>" } + let(:html) { "<span class=\"term-bold\">World</span>" } it_behaves_like 'stateable converter' end context "with split sequence" do let(:pre_text) { "\e[1m" } - let(:pre_html) { "<span class=\"term-bold\"></span>" } + let(:pre_html) { "" } let(:text) { "Hello" } let(:html) { "<span class=\"term-bold\">Hello</span>" } @@ -182,7 +182,7 @@ describe Gitlab::Ci::Ansi2html do context "with partial sequence" do let(:pre_text) { "Hello\e" } - let(:pre_html) { "Hello" } + let(:pre_html) { "<span class=\"\">Hello</span>" } let(:text) { "[1m World" } let(:html) { "<span class=\"term-bold\"> World</span>" } @@ -191,9 +191,9 @@ describe Gitlab::Ci::Ansi2html do context 'with new line' do let(:pre_text) { "Hello\r" } - let(:pre_html) { "Hello\r" } + let(:pre_html) { "<span class=\"\">Hello\r</span>" } let(:text) { "\nWorld" } - let(:html) { "<br>World" } + let(:html) { "<span class=\"\"><br/><span class=\"\">World</span></span>" } it_behaves_like 'stateable converter' end @@ -207,20 +207,20 @@ describe Gitlab::Ci::Ansi2html do let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"} let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"} let(:section_start_html) do - '<div class="hidden" data-action="start"'\ - " data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{section_name}\">"\ - "#{section_start[0...-5]}</div>" + '<div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer"' \ + " data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{class_name(section_name)}\"" \ + ' role="button"></div>' end let(:section_end_html) do - '<div class="hidden" data-action="end"'\ - " data-timestamp=\"#{section_end_time.to_i}\" data-section=\"#{section_name}\">"\ - "#{section_end[0...-5]}</div>" + "<div class=\"section-end\" data-section=\"#{class_name(section_name)}\"></div>" end shared_examples 'forbidden char in section_name' do it 'ignores sections' do text = "#{section_start}Some text#{section_end}" - html = text.gsub("\033[0K", '').gsub('<', '<') + class_name_start = section_start.gsub("\033[0K", '').gsub('<', '<') + class_name_end = section_end.gsub("\033[0K", '').gsub('<', '<') + html = %{<span class="">#{class_name_start}Some text#{class_name_end}</span>} expect(convert_html(text)).to eq(html) end @@ -231,7 +231,11 @@ describe Gitlab::Ci::Ansi2html do it 'prints light red' do text = "#{section_start}\e[91mHello\e[0m\n#{section_end}" - html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}} + header = %{<span class="term-fg-l-red section js-section-header js-s-#{class_name(section_name)}">Hello</span>} + line_break = %{<span class="section js-section-header js-s-#{class_name(section_name)}"><br/></span>} + line = %{<span class="section line s_#{class_name(section_name)}"></span>} + empty_line = %{<span class="section js-s-#{class_name(section_name)}"></span>} + html = "#{section_start_html}#{header}#{line_break}#{line}#{empty_line}#{section_end_html}" expect(convert_html(text)).to eq(html) end @@ -294,4 +298,8 @@ describe Gitlab::Ci::Ansi2html do stream = StringIO.new(data) subject.convert(stream).html end + + def class_name(section) + subject::Converter.new.section_to_class_name(section) + end end diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index e45ea1c2528..35250632e86 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -64,7 +64,10 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do result = stream.html - expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>") + expect(result).to eq( + "<span class=\"\">ヾ(´༎ຶД༎ຶ`)ノ<br/><span class=\"\"></span></span>"\ + "<span class=\"term-fg-green\">許功蓋</span><span class=\"\"><br/>"\ + "<span class=\"\"></span></span>") expect(result.encoding).to eq(Encoding.default_external) end end @@ -250,7 +253,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do it 'returns html content with state' do result = stream.html_with_state - expect(result.html).to eq("1234") + expect(result.html).to eq("<span class=\"\">1234</span>") end context 'follow-up state' do @@ -266,7 +269,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do result = stream.html_with_state(last_result.state) expect(result.append).to be_truthy - expect(result.html).to eq("5678") + expect(result.html).to eq("<span class=\"\">5678</span>") end end end @@ -302,11 +305,13 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do describe '#html' do shared_examples_for 'htmls' do it "returns html" do - expect(stream.html).to eq("12<br>34<br>56") + expect(stream.html).to eq( + "<span class=\"\">12<br/><span class=\"\">34<br/>"\ + "<span class=\"\">56</span></span></span>") end it "returns html for last line only" do - expect(stream.html(last_lines: 1)).to eq("56") + expect(stream.html(last_lines: 1)).to eq("<span class=\"\">56</span>") end end diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb index db935bcb388..f985b2dcbba 100644 --- a/spec/support/shared_examples/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/ci_trace_shared_examples.rb @@ -5,11 +5,11 @@ shared_examples_for 'common trace features' do end it "returns formatted html" do - expect(trace.html).to eq("12<br>34") + expect(trace.html).to eq("<span class=\"\">12<br/><span class=\"\">34</span></span>") end it "returns last line of formatted html" do - expect(trace.html(last_lines: 1)).to eq("34") + expect(trace.html(last_lines: 1)).to eq("<span class=\"\">34</span>") end end |