Commit 3b291822 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'remove-ff-job-log-json' into 'master'

Remove FF job_log_json flag from BE and FE

See merge request gitlab-org/gitlab!34538
parents 8812a456 9ad5bb11
...@@ -17,7 +17,7 @@ import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue'; ...@@ -17,7 +17,7 @@ import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue';
import Sidebar from './sidebar.vue'; import Sidebar from './sidebar.vue';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import delayedJobMixin from '../mixins/delayed_job_mixin'; import delayedJobMixin from '../mixins/delayed_job_mixin';
import { isNewJobLogActive } from '../store/utils'; import Log from './log/log.vue';
export default { export default {
name: 'JobPageApp', name: 'JobPageApp',
...@@ -28,7 +28,7 @@ export default { ...@@ -28,7 +28,7 @@ export default {
EnvironmentsBlock, EnvironmentsBlock,
ErasedBlock, ErasedBlock,
Icon, Icon,
Log: () => (isNewJobLogActive() ? import('./log/log.vue') : import('./job_log.vue')), Log,
LogTopBar, LogTopBar,
StuckBlock, StuckBlock,
UnmetPrerequisitesBlock, UnmetPrerequisitesBlock,
......
<script>
import { mapState, mapActions } from 'vuex';
export default {
name: 'JobLog',
props: {
trace: {
type: String,
required: true,
},
isComplete: {
type: Boolean,
required: true,
},
},
computed: {
...mapState(['isScrolledToBottomBeforeReceivingTrace']),
},
updated() {
this.$nextTick(() => {
this.handleScrollDown();
});
},
mounted() {
this.$nextTick(() => {
this.handleScrollDown();
});
},
methods: {
...mapActions(['scrollBottom']),
/**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown() {
if (this.isScrolledToBottomBeforeReceivingTrace) {
setTimeout(() => {
this.scrollBottom();
}, 0);
}
},
},
};
</script>
<template>
<pre class="js-build-trace build-trace qa-build-trace">
<code class="bash" v-html="trace">
</code>
<div v-if="!isComplete" class="js-log-animation build-loader-animation">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</pre>
</template>
import Vue from 'vue'; import Vue from 'vue';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { logLinesParser, updateIncrementalTrace, isNewJobLogActive } from './utils'; import { logLinesParser, updateIncrementalTrace } from './utils';
export default { export default {
[types.SET_JOB_ENDPOINT](state, endpoint) { [types.SET_JOB_ENDPOINT](state, endpoint) {
...@@ -25,22 +25,16 @@ export default { ...@@ -25,22 +25,16 @@ export default {
} }
if (log.append) { if (log.append) {
if (isNewJobLogActive()) {
state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace; state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
} else {
state.trace += log.html;
}
state.traceSize += log.size; state.traceSize += log.size;
} else { } else {
// When the job still does not have a trace // When the job still does not have a trace
// the trace response will not have a defined // the trace response will not have a defined
// html or size. We keep the old value otherwise these // html or size. We keep the old value otherwise these
// will be set to `null` // will be set to `null`
if (isNewJobLogActive()) {
state.trace = log.lines ? logLinesParser(log.lines) : state.trace; state.trace = log.lines ? logLinesParser(log.lines) : state.trace;
} else {
state.trace = log.html || state.trace;
}
state.traceSize = log.size || state.traceSize; state.traceSize = log.size || state.traceSize;
} }
......
import { isNewJobLogActive } from './utils';
export default () => ({ export default () => ({
jobEndpoint: null, jobEndpoint: null,
traceEndpoint: null, traceEndpoint: null,
...@@ -18,7 +16,7 @@ export default () => ({ ...@@ -18,7 +16,7 @@ export default () => ({
// Used to check if we should keep the automatic scroll // Used to check if we should keep the automatic scroll
isScrolledToBottomBeforeReceivingTrace: true, isScrolledToBottomBeforeReceivingTrace: true,
trace: isNewJobLogActive() ? [] : '', trace: [],
isTraceComplete: false, isTraceComplete: false,
traceSize: 0, traceSize: 0,
isTraceSizeVisible: false, isTraceSizeVisible: false,
......
...@@ -177,5 +177,3 @@ export const updateIncrementalTrace = (newLog = [], oldParsed = []) => { ...@@ -177,5 +177,3 @@ export const updateIncrementalTrace = (newLog = [], oldParsed = []) => {
return logLinesParser(newLog, parsedLog); return logLinesParser(newLog, parsedLog);
}; };
export const isNewJobLogActive = () => gon && gon.features && gon.features.jobLogJson;
...@@ -11,9 +11,6 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -11,9 +11,6 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_erase_build!, only: [:erase] before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize] before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action only: [:show] do
push_frontend_feature_flag(:job_log_json, project, default_enabled: true)
end
before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize
before_action :verify_proxy_request!, only: :proxy_websocket_authorize before_action :verify_proxy_request!, only: :proxy_websocket_authorize
...@@ -55,15 +52,10 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -55,15 +52,10 @@ class Projects::JobsController < Projects::ApplicationController
format.json do format.json do
build.trace.being_watched! build.trace.being_watched!
# TODO: when the feature flag is removed we should not pass
# content_format to serialize method.
content_format = Feature.enabled?(:job_log_json, @project, default_enabled: true) ? :json : :html
build_trace = Ci::BuildTrace.new( build_trace = Ci::BuildTrace.new(
build: @build, build: @build,
stream: stream, stream: stream,
state: params[:state], state: params[:state])
content_format: content_format)
render json: BuildTraceSerializer render json: BuildTraceSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
......
...@@ -2,40 +2,22 @@ ...@@ -2,40 +2,22 @@
module Ci module Ci
class BuildTrace class BuildTrace
CONVERTERS = {
html: Gitlab::Ci::Ansi2html,
json: Gitlab::Ci::Ansi2json
}.freeze
attr_reader :trace, :build attr_reader :trace, :build
delegate :state, :append, :truncated, :offset, :size, :total, to: :trace, allow_nil: true delegate :state, :append, :truncated, :offset, :size, :total, to: :trace, allow_nil: true
delegate :id, :status, :complete?, to: :build, prefix: true delegate :id, :status, :complete?, to: :build, prefix: true
def initialize(build:, stream:, state:, content_format:) def initialize(build:, stream:, state:)
@build = build @build = build
@content_format = content_format
if stream.valid? if stream.valid?
stream.limit stream.limit
@trace = CONVERTERS.fetch(content_format).convert(stream.stream, state) @trace = Gitlab::Ci::Ansi2json.convert(stream.stream, state)
end
end
def json?
@content_format == :json
end end
def html?
@content_format == :html
end
def json_lines
@trace&.lines if json?
end end
def html_lines def lines
@trace&.html if html? @trace&.lines
end end
end end
end end
...@@ -12,6 +12,5 @@ class BuildTraceEntity < Grape::Entity ...@@ -12,6 +12,5 @@ class BuildTraceEntity < Grape::Entity
expose :size expose :size
expose :total expose :total
expose :json_lines, as: :lines, if: ->(*) { object.json? } expose :lines
expose :html_lines, as: :html, if: ->(*) { object.html? }
end end
---
title: Remove legacy job log rendering
merge_request: 34538
author:
type: other
...@@ -13,8 +13,6 @@ module QA ...@@ -13,8 +13,6 @@ module QA
after do after do
@runner.remove_via_api! if @runner @runner.remove_via_api! if @runner
Runtime::Feature.enable('job_log_json') if @job_log_json_flag_enabled
end end
before do before do
......
...@@ -13,8 +13,6 @@ module QA ...@@ -13,8 +13,6 @@ module QA
after(:all) do after(:all) do
@runner.remove_via_api! @runner.remove_via_api!
Runtime::Feature.enable('job_log_json') if @job_log_json_flag_enabled
end end
before(:all) do before(:all) do
......
...@@ -646,109 +646,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -646,109 +646,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end end
end end
describe 'GET legacy trace.json' do
before do
stub_feature_flags(job_log_json: false)
get_trace
end
context 'when job has a trace artifact' do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'returns a trace' 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['state']).to be_present
expect(json_response['append']).not_to be_nil
expect(json_response['truncated']).not_to be_nil
expect(json_response['size']).to be_present
expect(json_response['total']).to be_present
expect(json_response['html']).to eq(job.trace.html)
end
end
context 'when job has a trace' do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
it 'returns a trace' 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('<span>BUILD TRACE</span>')
end
end
context 'when job has no traces' do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'returns no traces' 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 be_nil
end
end
context 'when job has a trace with ANSI sequence and Unicode' do
let(:job) { create(:ci_build, :unicode_trace_live, pipeline: pipeline) }
it 'returns a trace with Unicode' 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 include("ヾ(´༎ຶД༎ຶ`)ノ")
end
end
context 'when trace artifact is in ObjectStorage' do
let(:url) { 'http://object-storage/trace' }
let(:file_path) { expand_fixture_path('trace/sample_trace') }
let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
before do
allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
allow_any_instance_of(JobArtifactUploader).to receive(:url) { url }
allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) }
end
context 'when there are no network issues' do
before do
stub_remote_url_206(url, file_path)
get_trace
end
it 'returns a trace' 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(job.trace.html)
end
end
context 'when there is a network issue' do
before do
stub_remote_url_500(url)
end
it 'returns a trace' do
expect { get_trace }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError)
end
end
end
def get_trace
get :trace,
params: {
namespace_id: project.namespace,
project_id: project,
id: job.id
},
format: :json
end
end
describe 'GET status.json' do describe 'GET status.json' do
let(:job) { create(:ci_build, pipeline: pipeline) } let(:job) { create(:ci_build, pipeline: pipeline) }
let(:status) { job.detailed_status(double('user')) } let(:status) { job.detailed_status(double('user')) }
......
...@@ -837,7 +837,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -837,7 +837,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title]) expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-trace') expect(page).not_to have_selector('.job-log')
expect(page).to have_content('This job has been canceled') expect(page).to have_content('This job has been canceled')
end end
end end
...@@ -852,7 +852,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -852,7 +852,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title]) expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-trace') expect(page).not_to have_selector('.job-log')
expect(page).to have_content('This job has been skipped') expect(page).to have_content('This job has been skipped')
end end
end end
......
...@@ -397,106 +397,6 @@ describe('Job App', () => { ...@@ -397,106 +397,6 @@ describe('Job App', () => {
}); });
}); });
describe('trace output', () => {
describe('with append flag', () => {
it('appends the log content to the existing one', () =>
setupAndMount({
traceData: {
html: '<span>More<span>',
status: 'running',
state: 'newstate',
append: true,
complete: true,
},
})
.then(() => {
store.state.trace = 'Update';
return wrapper.vm.$nextTick();
})
.then(() => {
expect(
wrapper
.find('.js-build-trace')
.text()
.trim(),
).toEqual('Update');
}));
});
describe('without append flag', () => {
it('replaces the trace', () =>
setupAndMount({
traceData: {
html: '<span>Different<span>',
status: 'running',
append: false,
complete: true,
},
}).then(() => {
expect(
wrapper
.find('.js-build-trace')
.text()
.trim(),
).toEqual('Different');
}));
});
describe('truncated information', () => {
describe('when size is less than total', () => {
it('shows information about truncated log', () => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: true,
});
return setupAndMount({
traceData: {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: true,
},
}).then(() => {
expect(
wrapper
.find('.js-truncated-info')
.text()
.trim(),
).toContain('Showing last 50 bytes');
});
});
});
describe('when size is equal than total', () => {
it('does not show the truncated information', () =>
setupAndMount({
traceData: {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
complete: true,
},
}).then(() => {
expect(
wrapper
.find('.js-truncated-info')
.text()
.trim(),
).toEqual('');
}));
});
});
describe('trace controls', () => { describe('trace controls', () => {
beforeEach(() => beforeEach(() =>
setupAndMount({ setupAndMount({
...@@ -524,5 +424,4 @@ describe('Job App', () => { ...@@ -524,5 +424,4 @@ describe('Job App', () => {
expect(wrapper.find('.js-erase-link').exists()).toBe(true); expect(wrapper.find('.js-erase-link').exists()).toBe(true);
}); });
}); });
});
}); });
import Vue from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import component from '~/jobs/components/job_log.vue';
import createStore from '~/jobs/store';
import { resetStore } from '../store/helpers';
describe('Job Log', () => {
const Component = Vue.extend(component);
let store;
let vm;
const trace =
'<span>Running with gitlab-runner 12.1.0 (de7731dd)<br/></span><span> on docker-auto-scale-com d5ae8d25<br/></span><div class="gl-mr-3" data-timestamp="1565502765" data-section="prepare-executor" role="button"></div><span class="section section-header js-s-prepare-executor">Using Docker executor with image ruby:2.6 ...<br/></span>';
beforeEach(() => {
store = createStore();
});
afterEach(() => {
resetStore(store);
vm.$destroy();
});
it('renders provided trace', () => {
vm = mountComponentWithStore(Component, {
props: {
trace,
isComplete: true,
},
store,
});
expect(vm.$el.querySelector('code').textContent).toContain(
'Running with gitlab-runner 12.1.0 (de7731dd)',
);
});
describe('while receiving trace', () => {
it('renders animation', () => {
vm = mountComponentWithStore(Component, {
props: {
trace,
isComplete: false,
},
store,
});
expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull();
});
});
describe('when build trace has finishes', () => {
it('does not render animation', () => {
vm = mountComponentWithStore(Component, {
props: {
trace,
isComplete: true,
},
store,
});
expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
});
});
});
...@@ -76,28 +76,15 @@ describe('Jobs Store Mutations', () => { ...@@ -76,28 +76,15 @@ describe('Jobs Store Mutations', () => {
lines: [], lines: [],
}); });
expect(stateCopy.trace).toEqual(html);
expect(stateCopy.traceSize).toEqual(511846); expect(stateCopy.traceSize).toEqual(511846);
expect(stateCopy.isTraceComplete).toEqual(true); expect(stateCopy.isTraceComplete).toEqual(true);
}); });
describe('with new job log', () => { describe('with new job log', () => {
let stateWithNewLog;
beforeEach(() => {
gon.features = gon.features || {};
gon.features.jobLogJson = true;
stateWithNewLog = state();
});
afterEach(() => {
gon.features.jobLogJson = false;
});
describe('log.lines', () => { describe('log.lines', () => {
describe('when append is true', () => { describe('when append is true', () => {
it('sets the parsed log ', () => { it('sets the parsed log ', () => {
mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, { mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
append: true, append: true,
size: 511846, size: 511846,
complete: true, complete: true,
...@@ -109,7 +96,7 @@ describe('Jobs Store Mutations', () => { ...@@ -109,7 +96,7 @@ describe('Jobs Store Mutations', () => {
], ],
}); });
expect(stateWithNewLog.trace).toEqual([ expect(stateCopy.trace).toEqual([
{ {
offset: 1, offset: 1,
content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
...@@ -121,7 +108,7 @@ describe('Jobs Store Mutations', () => { ...@@ -121,7 +108,7 @@ describe('Jobs Store Mutations', () => {
describe('when it is defined', () => { describe('when it is defined', () => {
it('sets the parsed log ', () => { it('sets the parsed log ', () => {
mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, { mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
append: false, append: false,
size: 511846, size: 511846,
complete: true, complete: true,
...@@ -130,7 +117,7 @@ describe('Jobs Store Mutations', () => { ...@@ -130,7 +117,7 @@ describe('Jobs Store Mutations', () => {
], ],
}); });
expect(stateWithNewLog.trace).toEqual([ expect(stateCopy.trace).toEqual([
{ {
offset: 0, offset: 0,
content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }], content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
...@@ -142,7 +129,7 @@ describe('Jobs Store Mutations', () => { ...@@ -142,7 +129,7 @@ describe('Jobs Store Mutations', () => {
describe('when it is null', () => { describe('when it is null', () => {
it('sets the default value', () => { it('sets the default value', () => {
mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, { mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
append: true, append: true,
html, html,
size: 511846, size: 511846,
...@@ -150,7 +137,7 @@ describe('Jobs Store Mutations', () => { ...@@ -150,7 +137,7 @@ describe('Jobs Store Mutations', () => {
lines: null, lines: null,
}); });
expect(stateWithNewLog.trace).toEqual([]); expect(stateCopy.trace).toEqual([]);
}); });
}); });
}); });
......
...@@ -11,7 +11,7 @@ RSpec.describe Ci::BuildTrace do ...@@ -11,7 +11,7 @@ RSpec.describe Ci::BuildTrace do
Gitlab::Ci::Trace::Stream.new { data } Gitlab::Ci::Trace::Stream.new { data }
end end
subject { described_class.new(build: build, stream: stream, state: state, content_format: content_format) } subject { described_class.new(build: build, stream: stream, state: state) }
shared_examples 'delegates methods' do shared_examples 'delegates methods' do
it { is_expected.to delegate_method(:state).to(:trace) } it { is_expected.to delegate_method(:state).to(:trace) }
...@@ -25,29 +25,11 @@ RSpec.describe Ci::BuildTrace do ...@@ -25,29 +25,11 @@ RSpec.describe Ci::BuildTrace do
it { is_expected.to delegate_method(:complete?).to(:build).with_prefix } it { is_expected.to delegate_method(:complete?).to(:build).with_prefix }
end end
context 'with :json content format' do
let(:content_format) { :json }
it_behaves_like 'delegates methods' it_behaves_like 'delegates methods'
it { is_expected.to be_json }
it 'returns formatted trace' do it 'returns formatted trace' do
expect(subject.trace.lines).to eq([ expect(subject.lines).to eq([
{ offset: 0, content: [{ text: 'the-stream' }] } { offset: 0, content: [{ text: 'the-stream' }] }
]) ])
end end
end
context 'with :html content format' do
let(:content_format) { :html }
it_behaves_like 'delegates methods'
it { is_expected.to be_html }
it 'returns formatted trace' do
expect(subject.trace.html).to eq('<span>the-stream</span>')
end
end
end end
...@@ -13,7 +13,7 @@ RSpec.describe BuildTraceEntity do ...@@ -13,7 +13,7 @@ RSpec.describe BuildTraceEntity do
end end
let(:build_trace) do let(:build_trace) do
Ci::BuildTrace.new(build: build, stream: stream, content_format: content_format, state: nil) Ci::BuildTrace.new(build: build, stream: stream, state: nil)
end end
let(:entity) do let(:entity) do
...@@ -22,7 +22,6 @@ RSpec.describe BuildTraceEntity do ...@@ -22,7 +22,6 @@ RSpec.describe BuildTraceEntity do
subject { entity.as_json } subject { entity.as_json }
shared_examples 'includes build and trace metadata' do
it 'includes build attributes' do it 'includes build attributes' do
expect(subject[:id]).to eq(build.id) expect(subject[:id]).to eq(build.id)
expect(subject[:status]).to eq(build.status) expect(subject[:status]).to eq(build.status)
...@@ -37,27 +36,10 @@ RSpec.describe BuildTraceEntity do ...@@ -37,27 +36,10 @@ RSpec.describe BuildTraceEntity do
expect(subject).to include(:size) expect(subject).to include(:size)
expect(subject).to include(:total) expect(subject).to include(:total)
end end
end
context 'when content format is :json' do
let(:content_format) { :json }
it_behaves_like 'includes build and trace metadata'
it 'includes the trace content in json' do it 'includes the trace content in json' do
expect(subject[:lines]).to eq([ expect(subject[:lines]).to eq([
{ offset: 0, content: [{ text: 'the-trace' }] } { offset: 0, content: [{ text: 'the-trace' }] }
]) ])
end end
end
context 'when content format is :html' do
let(:content_format) { :html }
it_behaves_like 'includes build and trace metadata'
it 'includes the trace content in json' do
expect(subject[:html]).to eq('<span>the-trace</span>')
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment