diff --git a/composer/src/main/kotlin/com/gojuno/composer/html/HtmlReport.kt b/composer/src/main/kotlin/com/gojuno/composer/html/HtmlReport.kt index ce69edc..ea8d49f 100644 --- a/composer/src/main/kotlin/com/gojuno/composer/html/HtmlReport.kt +++ b/composer/src/main/kotlin/com/gojuno/composer/html/HtmlReport.kt @@ -95,7 +95,7 @@ fun generateLogcatHtml(logcatOutput: File): String = when (logcatOutput.exists() true -> logcatOutput .readLines() .map { line -> """
${StringEscapeUtils.escapeXml11(line)}
""" } - .fold(StringBuilder("""
""")) { stringBuilder, line -> + .fold(StringBuilder("""
""")) { stringBuilder, line -> stringBuilder.appendln(line) } .appendln("""
""") diff --git a/html-report/layout/log-container.html b/html-report/layout/log-container.html index e34bcc2..52400c0 100644 --- a/html-report/layout/log-container.html +++ b/html-report/layout/log-container.html @@ -1,4 +1,4 @@ -
+
${log_entries}
diff --git a/html-report/src/components/LogContainer.js b/html-report/src/components/LogContainer.js new file mode 100644 index 0000000..5b1317a --- /dev/null +++ b/html-report/src/components/LogContainer.js @@ -0,0 +1,61 @@ +import React, { Component } from 'react'; +import cx from 'classnames'; +import LogFilter from './LogFilter' + +export default class LogContainer extends Component { + + state = { + logs: [], + results: [], + loading: true, + hide: false + } + + componentDidMount() { + document.addEventListener('DOMContentLoaded', (event) => { + this.loadStaticLogs() + }) + } + + loadStaticLogs() { + let logDivs = document.querySelectorAll('#static_logs > div > div') + if (!logDivs.length) { + this.setState({hide: true, loading: false}) + return + } + let logs = [] + for (var div of logDivs) { + logs.push(Object.assign({level: div.className, line: div.innerText, key: logs.length})) + } + this.setState({logs: logs, results: logs, loading: false}) + window.document.getElementById("static_logs").remove() + } + + getSearchResults(results) { + this.setState({ results: results }); + } + + render() { + return this.state.hide + ? (
) + : ( +
+ this.getSearchResults(results) } + data={this.state.logs} + /> + { !this.state.loading && +
+ { this.state.results.map((entry, i) => { + return ( +
+ { entry.line } +
+ ) + }) + } +
} +
+ ); + } +} diff --git a/html-report/src/components/LogFilter.js b/html-report/src/components/LogFilter.js new file mode 100644 index 0000000..97f33fa --- /dev/null +++ b/html-report/src/components/LogFilter.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import SearchBar from './SearchBar'; +import LogFilterButton from './LogFilterButton'; + +const SEARCH_FIELDS = ['level', 'line']; +const SEARCH_REF = 'line'; +export default class LogFilter extends Component { + static propTypes = { + setSearchResults: PropTypes.func, + data: PropTypes.array + }; + + render() { + return ( +
+
+
LOGS
+ this.performFilterSearch("level:verbose") } text="Verbose" disabled={ !!!this.props.data.length } /> + this.performFilterSearch("level:debug") } text="Debug" disabled={ !!!this.props.data.length } /> + this.performFilterSearch("level:info") } text="Info" disabled={ !!!this.props.data.length } /> + this.performFilterSearch("level:warning") } text="Warning" disabled={ !!!this.props.data.length } /> + this.performFilterSearch("level:error") } text="Error" disabled={ !!!this.props.data.length } /> + this.performFilterSearch("level:assert") } text="Assert" disabled={ !!!this.props.data.length } /> +
+ (this.performFilterSearch = callback) } + /> +
+ ); + } +} diff --git a/html-report/src/components/LogFilterButton.js b/html-report/src/components/LogFilterButton.js new file mode 100644 index 0000000..d368b47 --- /dev/null +++ b/html-report/src/components/LogFilterButton.js @@ -0,0 +1,13 @@ +import React from 'react'; + +const LogFilterButton = ({text, disabled, onClick}) => ( + +); + +export default LogFilterButton; diff --git a/html-report/src/components/SearchBar.js b/html-report/src/components/SearchBar.js index 2bc7375..6816ecf 100644 --- a/html-report/src/components/SearchBar.js +++ b/html-report/src/components/SearchBar.js @@ -4,16 +4,18 @@ import cx from 'classnames'; import elasticlunr from 'elasticlunr'; import convertTime from './../utils/convertTime' -const SEARCH_FIELDS = ['package_name', 'class_name', 'name', 'id', 'status']; -const SEARCH_REF = 'id'; const EL_SEARCH = elasticlunr(); export default class SearchBar extends Component { static propTypes = { - setSearchResults: PropTypes.func + setSearchResults: PropTypes.func, + setPerformFilterSearchCallback: PropTypes.func, + searchFields: PropTypes.array, + searchRef: PropTypes.string, + data: PropTypes.array, }; state = { - data: window.suite.tests, + data: this.props.data, error: false, searchLabel: null, searchParams: null, @@ -21,13 +23,28 @@ export default class SearchBar extends Component { }; componentWillMount() { - let { data } = this.state; elasticlunr.clearStopWords(); - SEARCH_FIELDS.forEach(f => EL_SEARCH.addField(f)) - EL_SEARCH.setRef(SEARCH_REF); - if (data.length) { - data.forEach(item => EL_SEARCH.addDoc(item)) - } + this.props.searchFields.forEach(f => EL_SEARCH.addField(f)) + EL_SEARCH.setRef(this.props.searchRef); + } + + componentDidMount() { + this.props.setPerformFilterSearchCallback(this.performFilterSearch) + } + + componentWillReceiveProps(props) { + let data = props.data; + if (data === this.state.data) { + return; + } + if (data.length) { + data.forEach(item => EL_SEARCH.addDoc(item)) + } + this.setState({data: props.data}); + } + + componentWillUnmount() { + this.props.setPerformFilterSearchCallback(undefined) } mapResults(results) { @@ -42,14 +59,14 @@ export default class SearchBar extends Component { }; setTagSearch = (field, callback) => { - if (SEARCH_FIELDS.indexOf(field) < 0) { + if (this.props.searchFields.indexOf(field) < 0) { this.setState({ error: true }); return; } let params = {}; params.fields = {}; - SEARCH_FIELDS.forEach((f) => { + this.props.searchFields.forEach((f) => { if (f === field) { params.fields[f] = { boost: 1 } } else { @@ -97,43 +114,29 @@ export default class SearchBar extends Component { render() { let errorTextClasses = cx('form-item__error-text col-100', { visible: this.state.error }); let errorInputClasses = cx({ 'is-invalid-input': this.state.error }); - const data = window.suite; return ( -
-
-
this.performFilterSearch('status:passed') }> -
Passed
-
{ data.passed_count }
-
-
this.performFilterSearch('status:failed') }> -
Failed
-
{ data.failed_count }
-
-
this.performFilterSearch('status:ignored') }> -
Ignored
-
{ data.ignored_count }
-
-
-
Duration
-
{ convertTime(data.duration_millis) }
-
-
-
-
-
-
-
-
- { this.state.searchLabel &&
{ this.state.searchLabel }:
} - - -
-
No such key exists!
+
+
+
+
+
+
+ { this.state.searchLabel &&
{ this.state.searchLabel }:
} + +
+
No such key exists!
diff --git a/html-report/src/components/Suite.js b/html-report/src/components/Suite.js index 1b86d84..28e7e95 100644 --- a/html-report/src/components/Suite.js +++ b/html-report/src/components/Suite.js @@ -3,7 +3,7 @@ import cx from 'classnames'; import randomColor from 'randomcolor'; import convertTime from './../utils/convertTime'; import paths from './../utils/paths'; -import SearchBar from './SearchBar'; +import SuiteFilter from './SuiteFilter'; export default class Suite extends Component { state = { @@ -38,7 +38,7 @@ export default class Suite extends Component {
Suites list/ Suite {data.id}
- this.getSearchResults(results) } /> + this.getSearchResults(results) } />
diff --git a/html-report/src/components/SuiteFilter.js b/html-report/src/components/SuiteFilter.js new file mode 100644 index 0000000..d4691c1 --- /dev/null +++ b/html-report/src/components/SuiteFilter.js @@ -0,0 +1,51 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import convertTime from './../utils/convertTime' +import SearchBar from './SearchBar'; + +const SEARCH_FIELDS = ['package_name', 'class_name', 'name', 'id', 'status']; +const SEARCH_REF = 'id'; +export default class SuiteFilter extends Component { + static propTypes = { + setSearchResults: PropTypes.func + }; + + performFilterSearch = (query) => { + this.searchBar.performFilterSearch(query) + }; + + render() { + const data = window.suite; + + return ( +
+
+
this.performFilterSearch('status:passed') }> +
Passed
+
{ data.passed_count }
+
+
this.performFilterSearch('status:failed') }> +
Failed
+
{ data.failed_count }
+
+
this.performFilterSearch('status:ignored') }> +
Ignored
+
{ data.ignored_count }
+
+
+
Duration
+
{ convertTime(data.duration_millis) }
+
+
+ (this.performFilterSearch = callback) } + /> +
+ ) + } +} diff --git a/html-report/src/components/TestItem.js b/html-report/src/components/TestItem.js index cbff2ea..642f914 100644 --- a/html-report/src/components/TestItem.js +++ b/html-report/src/components/TestItem.js @@ -2,14 +2,21 @@ import React, { Component } from 'react'; import cx from 'classnames'; import convertTime from './../utils/convertTime' import paths from './../utils/paths' +import LogContainer from './LogContainer' export default class TestItem extends Component { + + state = { + data: window.test, + }; + componentWillMount() { document.title = `Test ${window.test.name}`; } render() { - const data = window.test; + const data = this.state.data + let statusLabelClass = cx('label', 'margin-right-10', { alert: data.status === 'failed', success: data.status === 'passed' @@ -65,6 +72,8 @@ export default class TestItem extends Component { }) }
} + +
);