├── data
├── thing.txt
├── invalid.xml
├── subfolder
│ └── _thingy.xml
├── utf-16.xml
├── complete_single_case_only.xml
├── no_class_name.xml
├── passing_suite.xml
├── name.has.dots.xml
├── skipped_suite.xml
├── issue_2.xml
├── complete_no_suite_multi_cases.xml
├── with_html.xml
├── error_suite.xml
├── failing_suite.xml
├── pytest_testcase_properties.xml
├── multi_error_test_with_system_out.xml
├── defect_suite.xml
├── malformed.xml
├── nested-nested.xml
├── issue_3.xml
├── russian-unicode.xml
├── multi_cases.xml
├── properties_in_test_meta.xml
├── special_chars_suite.xml
├── semi-colon.xml
├── suite-system-out.xml
├── class_not_classname.xml
├── test-system-out.xml
├── multi-name-unique-classname.xml
├── duplicate_name_unique_classanme.xml
├── multi_suite.xml
├── complete_no_suite_single_suite.xml
├── most_complex.xml
├── test.xml
├── complete_no_suite.xml
├── embedded_html_sysout.xml
└── complete_single_suite.xml
├── .env
├── public
├── icon.png
├── favicon.ico
└── index.html
├── example-header.png
├── gh-pages
├── icon.png
└── favicon.ico
├── XunitViewerIcon.png
├── src
├── app
│ ├── parse.js
│ ├── logo.test.js
│ ├── toggle.test.js
│ ├── loading.js
│ ├── suite-options.test.js
│ ├── properties-options.test.js
│ ├── test-options.test.js
│ ├── __snapshots__
│ │ ├── toggle.test.js.snap
│ │ ├── logo.test.js.snap
│ │ ├── properties-options.test.js.snap
│ │ ├── suite-options.test.js.snap
│ │ ├── test-options.test.js.snap
│ │ └── hero.test.js.snap
│ ├── suite-count.js
│ ├── toggle.js
│ ├── logo.js
│ ├── error.js
│ ├── initial-state.js
│ ├── visible.js
│ ├── parse-all.js
│ ├── suite-options.js
│ ├── properties-options.js
│ ├── files.js
│ ├── app.js
│ ├── test-options.js
│ ├── hero.js
│ ├── suite.js
│ ├── reducer.js
│ └── index.css
├── cli
│ ├── watch.js
│ ├── parse.test.js
│ ├── get-description.js
│ ├── get-suites.test.js
│ ├── get-description.test.js
│ ├── get-suites.js
│ ├── update-expected.js
│ ├── server.js
│ ├── get-files.js
│ ├── render.js
│ ├── logger.js
│ ├── terminal.js
│ ├── static
│ │ └── js
│ │ │ └── main.4e6e0818.js.LICENSE.txt
│ ├── get-files.test.js
│ ├── index.html
│ ├── args.js
│ ├── parse.js
│ └── parse-expected.json
└── index.js
├── bin
└── xunit-viewer.js
├── sample-usage.js
├── .npmignore
├── .github
├── workflows
│ └── main.yml
└── ISSUE_TEMPLATE
│ └── issue.md
├── .gitignore
├── release.sh
├── XunitViewerIcon.svg
├── SECURITY.md
├── component
└── icon-map.jsx
├── junit.xml
├── LICENCE
├── xunit-viewer.js
├── package.json
└── README.md
/data/thing.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/invalid.xml:
--------------------------------------------------------------------------------
1 | bacon
--------------------------------------------------------------------------------
/data/subfolder/_thingy.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/data/utf-16.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukejpreston/xunit-viewer/HEAD/data/utf-16.xml
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukejpreston/xunit-viewer/HEAD/public/icon.png
--------------------------------------------------------------------------------
/example-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukejpreston/xunit-viewer/HEAD/example-header.png
--------------------------------------------------------------------------------
/gh-pages/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukejpreston/xunit-viewer/HEAD/gh-pages/icon.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukejpreston/xunit-viewer/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/XunitViewerIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukejpreston/xunit-viewer/HEAD/XunitViewerIcon.png
--------------------------------------------------------------------------------
/gh-pages/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukejpreston/xunit-viewer/HEAD/gh-pages/favicon.ico
--------------------------------------------------------------------------------
/src/app/parse.js:
--------------------------------------------------------------------------------
1 | import '../cli/parse.js'
2 | const parse = window.parse
3 | export default parse
4 |
--------------------------------------------------------------------------------
/bin/xunit-viewer.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import xunitViewer from '../xunit-viewer.js'
4 | import { args } from '../src/cli/args.js'
5 |
6 | xunitViewer(args)
7 |
--------------------------------------------------------------------------------
/data/complete_single_case_only.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/data/no_class_name.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/data/passing_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/data/name.has.dots.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/data/skipped_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample-usage.js:
--------------------------------------------------------------------------------
1 | import xunitViewer from './xunit-viewer'
2 |
3 | xunitViewer({
4 | server: false,
5 | results: 'data',
6 | ignore: ['_thingy', 'invalid'],
7 | title: 'Xunit View Sample Tests',
8 | output: 'output.html'
9 | })
10 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/__snapshots__
2 | src/app
3 | src/index.js
4 | data
5 | public
6 | scripts
7 | .env
8 | build
9 | coverage
10 | gh-pages
11 | junit.xml
12 | sample-usage.js
13 | XunitViewerIcon.png
14 | XunitViewerIcon.svg
15 | xunit-viewer-results.html
--------------------------------------------------------------------------------
/src/app/logo.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Logo from './logo'
3 | import renderer from 'react-test-renderer'
4 |
5 | test('renders logo', () => {
6 | const tree = renderer.create().toJSON()
7 | expect(tree).toMatchSnapshot()
8 | })
9 |
--------------------------------------------------------------------------------
/src/cli/watch.js:
--------------------------------------------------------------------------------
1 | import chokidar from 'chokidar'
2 | import debounce from 'debounce'
3 |
4 | export default ({ results }, cb) => {
5 | cb = debounce(cb)
6 | chokidar.watch(results)
7 | .on('all', (event, path) => {
8 | cb()
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/toggle.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Toggle from './toggle'
3 | import renderer from 'react-test-renderer'
4 |
5 | test('renders toggle', () => {
6 | const tree = renderer.create().toJSON()
7 | expect(tree).toMatchSnapshot()
8 | })
9 |
--------------------------------------------------------------------------------
/data/issue_2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/data/complete_no_suite_multi_cases.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/app/loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Loading = () => (
4 |
5 |
6 |
Parsing the files
7 |
8 | )
9 |
10 | export default Loading
11 |
--------------------------------------------------------------------------------
/src/app/suite-options.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SuiteOptions from './suite-options'
3 | import renderer from 'react-test-renderer'
4 |
5 | test('renders suite options', () => {
6 | const tree = renderer.create().toJSON()
7 | expect(tree).toMatchSnapshot()
8 | })
9 |
--------------------------------------------------------------------------------
/data/with_html.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <i>ARGG</i><b>BOO</b>
6 |
7 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push, pull_request]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2
8 | - uses: actions/setup-node@v1
9 | with:
10 | node-version: '12.x'
11 | - run: npm install
12 | - run: npm run lint
13 | - run: npm run test:ci
--------------------------------------------------------------------------------
/data/error_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | java.lang.RuntimeException: There was an error
5 |
6 |
--------------------------------------------------------------------------------
/src/app/properties-options.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropertiesOptions from './properties-options'
3 | import renderer from 'react-test-renderer'
4 |
5 | test('renders properties options', () => {
6 | const tree = renderer.create().toJSON()
7 | expect(tree).toMatchSnapshot()
8 | })
9 |
--------------------------------------------------------------------------------
/src/app/test-options.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestOptions from './test-options'
3 | import renderer from 'react-test-renderer'
4 |
5 | test('renders test options', () => {
6 | const tree = renderer.create().toJSON()
7 | expect(tree).toMatchSnapshot()
8 | })
9 |
--------------------------------------------------------------------------------
/data/failing_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | FILENAME:XX
Expected
<string>: Luke
to equal
<string>: luke
5 |
6 |
--------------------------------------------------------------------------------
/data/pytest_testcase_properties.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/data/multi_error_test_with_system_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Error message
5 | Some messgae
6 | FILENAME:XX
7 |
8 |
--------------------------------------------------------------------------------
/src/app/__snapshots__/toggle.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders toggle 1`] = `
4 |
20 | `;
21 |
--------------------------------------------------------------------------------
/src/cli/parse.test.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import './parse'
4 | import expected from './parse-expected.json'
5 |
6 | const parse = window.parse
7 | const dataDir = path.resolve(__dirname, '../../data')
8 |
9 | test('complete multi suites', async () => {
10 | const data = path.join(dataDir, '/test.xml')
11 | const result = await parse(fs.readFileSync(data).toString())
12 | expect(result).toEqual(expected)
13 | })
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # IDEs
4 | .idea
5 |
6 | # dependencies
7 | /node_modules
8 | /.pnp
9 | .pnp.js
10 |
11 | # testing
12 | /coverage
13 | /output
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
--------------------------------------------------------------------------------
/src/cli/get-description.js:
--------------------------------------------------------------------------------
1 | export default (suites) => {
2 | const testCounts = {}
3 | Object.values(suites.suites).forEach((suite) => {
4 | Object.values(suite.tests).forEach((test) => {
5 | const status = test.status || 'unknown'
6 | testCounts[status] = testCounts[status] || 0
7 | testCounts[status] += 1
8 | })
9 | })
10 | return Object.entries(testCounts)
11 | .map(([status, count]) => {
12 | return `${count} ${status}`
13 | })
14 | .join(', ')
15 | }
16 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Xunit Viewer
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/app/suite-count.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const icons = {
4 | passed: 'check',
5 | failure: 'times',
6 | error: 'exclamation',
7 | skipped: 'ban',
8 | unknown: 'question'
9 | }
10 |
11 | const SuiteCount = ({ count, type }) => count > 0
12 | ?
13 |
14 |
15 |
16 | {count}
17 |
18 | : null
19 |
20 | export default SuiteCount
21 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | npm run lint
6 | npm run build:cli
7 | npm run test:ci
8 | ./bin/xunit-viewer.js -r junit.xml -c -C false
9 | CURRENT=$(echo $(npm version | grep xunit-viewer | cut -d"'" -f4))
10 | git commit --allow-empty -am "tested $CURRENT"
11 |
12 | npm version ${1-patch}
13 | npm publish
14 | LATEST=$(echo npm version | grep xunit-viewer | cut -d"'" -f4)
15 |
16 | npm run release:demo
17 |
18 | git add -A
19 | git commit --allow-empty -am "release demo $LATEST"
20 |
21 | git push
22 | git push --tags
23 |
--------------------------------------------------------------------------------
/src/cli/get-suites.test.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import getFiles from './get-files'
3 | import getSuites from './get-suites'
4 | import expected from './get-suites-expected.json'
5 |
6 | const logger = {
7 | warning: input => input,
8 | file: input => input,
9 | error: input => input
10 | }
11 |
12 | test('get suites', async () => {
13 | const files = await getFiles(logger, { results: path.resolve(__dirname, '../../data') })
14 | const suites = await getSuites(logger, files)
15 | expect(suites).toEqual(expected)
16 | })
17 |
--------------------------------------------------------------------------------
/src/cli/get-description.test.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import getDescription from './get-description.js'
4 | import './parse'
5 |
6 | const parse = window.parse
7 |
8 | const dataDir = path.resolve(__dirname, '../../data')
9 |
10 | test('get description', async () => {
11 | const data = path.join(dataDir, '/test.xml')
12 | const parsed = await parse(fs.readFileSync(data).toString())
13 | const result = getDescription(parsed)
14 | expect(result).toBe('13 passed, 2 failure, 1 error, 1 unknown, 1 skipped')
15 | })
16 |
--------------------------------------------------------------------------------
/src/cli/get-suites.js:
--------------------------------------------------------------------------------
1 | import merge from 'merge'
2 | import parse from './parse.js'
3 |
4 | let parseXml = parse
5 | if (typeof window !== 'undefined') parseXml = window.parse
6 |
7 | export default async (logger, files) => {
8 | let suites = {}
9 | for (const { file, contents } of files) {
10 | try {
11 | const res = await parseXml(contents)
12 | suites = merge.recursive(true, suites, res)
13 | } catch (err) {
14 | console.log(logger.error('Failed to parse'), logger.file(file), '\n', logger.error(err.message), '\n')
15 | }
16 | }
17 | return suites
18 | }
19 |
--------------------------------------------------------------------------------
/data/defect_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | java.lang.RuntimeException: There was an error
11 |
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue
3 | about: Raise an issue
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | Raise any issues using GitHub and provide sample data where possible.
11 |
12 | To help debug any issues please provide the following info
13 |
14 | * node and npm version
15 | * xunit viewer version
16 | * browser
17 | * sample xml
18 |
19 | If you have issue migrating from Junit Viewer or older version of Xunit Viewer please feel free to raise an issue titled **MIGRATION HELP**
20 |
21 | There is a `v5` branch which maintained through Open Source PRs, this branch will not maintain `npm audit` issues
22 |
--------------------------------------------------------------------------------
/src/app/toggle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Toggle = ({
4 | active,
5 | onIcon,
6 | offIcon,
7 | onLabel,
8 | offLabel,
9 | disabled = false,
10 | onChange = () => {},
11 | className = ''
12 | }) => {
13 | return
25 | }
26 |
27 | export default Toggle
28 |
--------------------------------------------------------------------------------
/XunitViewerIcon.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 5.1.x | :white_check_mark: |
11 | | 5.0.x | :x: |
12 | | 4.0.x | :white_check_mark: |
13 | | < 4.0 | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Use this section to tell people how to report a vulnerability.
18 |
19 | Tell them where to go, how often they can expect to get an update on a
20 | reported vulnerability, what to expect if the vulnerability is accepted or
21 | declined, etc.
22 |
--------------------------------------------------------------------------------
/component/icon-map.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Check from './icons/check'
3 | import Times from './icons/times'
4 | import Exclamation from './icons/exclamation'
5 | import Ban from './icons/ban'
6 | import Question from './icons/question'
7 | import AngleDown from './icons/angle-down'
8 | import AngleUp from './icons/angle-up'
9 |
10 | let Icon = ({ children }) => {
11 | return
12 | {children}
13 |
14 | }
15 |
16 | export default {
17 | pass: ,
18 | fail: ,
19 | error: ,
20 | skip: ,
21 | unknown: ,
22 | angleDown: ,
23 | angleUp:
24 | }
25 |
--------------------------------------------------------------------------------
/data/malformed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | java.lang.AssertionError
5 | at org.junit.Assert.fail(Assert.java:86)
6 | at org.junit.Assert.assertTrue(Assert.java:41)
7 | at org.junit.Assert.assertTrue(Assert.java:52)
8 | at com.germaniumhq.germanium.steps.GermaniumFunctionSelectFile.the_file_is_uploaded_successfully(GermaniumFunctionSelectFile.java:26)
9 | at ✽.Then the file is uploaded successfully(features/features/germanium-function-select_file.feature:7)
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/app/logo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Logo = () =>
10 |
11 | export default Logo
12 |
--------------------------------------------------------------------------------
/src/app/error.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const issueUrl = 'https://github.com/lukejpreston/xunit-viewer/issues/new?assignees=&labels=&template=issue.md&title='
4 |
5 | const Error = ({ errors }) => {
6 | return (
7 |
8 |
Errors found
9 |
There was an error parsing your data.
10 |
11 | {errors.map(({ file, error }) => (
12 | -
13 | {file}
14 |
15 | {error}
16 |
17 | ))}
18 |
19 |
20 | If you think this is an issue with the library please raise it here GitHub Issue
21 |
22 |
23 | )
24 | }
25 |
26 | export default Error
27 |
--------------------------------------------------------------------------------
/data/nested-nested.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/__snapshots__/logo.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders logo 1`] = `
4 |
38 | `;
39 |
--------------------------------------------------------------------------------
/data/issue_3.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/cli/update-expected.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import getFiles from './get-files'
4 | import getSuites from './get-suites'
5 | import parse from './parse'
6 | // const parse = window.parse
7 |
8 | import { fileURLToPath } from 'url'
9 | const __filename = fileURLToPath(import.meta.url)
10 | const __dirname = path.dirname(__filename)
11 |
12 | const logger = {
13 | warning: input => input,
14 | file: input => input,
15 | error: input => input
16 | }
17 |
18 | const main = async () => {
19 | const files = getFiles(logger, { results: path.resolve(__dirname, '../../data') })
20 | const suites = await getSuites(logger, files)
21 | fs.writeFileSync(path.resolve(__dirname, 'get-suites-expected.json'), JSON.stringify(suites, null, 2))
22 |
23 | const dataDir = path.resolve(__dirname, '../../data')
24 | const data = path.join(dataDir, '/test.xml')
25 | const result = await parse(fs.readFileSync(data).toString())
26 | fs.writeFileSync(path.resolve(__dirname, 'parse-expected.json'), JSON.stringify(result, null, 2))
27 | }
28 | main()
29 |
--------------------------------------------------------------------------------
/junit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/data/russian-unicode.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
--------------------------------------------------------------------------------
/src/app/initial-state.js:
--------------------------------------------------------------------------------
1 | export default {
2 | hero: {
3 | burger: false,
4 | dropdown: false,
5 | fastFilter: 0
6 | },
7 | printMode: false,
8 | error: null,
9 | suites: {},
10 | currentSuites: {},
11 | menuActive: false,
12 | suiteOptionsActive: false,
13 | testOptionsActive: false,
14 | propertiesOptionsActive: false,
15 | activeFiles: false,
16 | suitesExpanded: true,
17 | suitesEmpty: true,
18 | propertiesExpanded: {
19 | all: true,
20 | suites: true,
21 | tests: true
22 | },
23 | propertiesVisible: {
24 | all: true,
25 | suites: true,
26 | tests: true
27 | },
28 | testToggles: {
29 | all: {
30 | expanded: true,
31 | raw: true
32 | },
33 | passed: {
34 | expanded: true,
35 | raw: true
36 | },
37 | skipped: {
38 | expanded: true,
39 | raw: true
40 | },
41 | failure: {
42 | expanded: true,
43 | raw: true
44 | },
45 | error: {
46 | expanded: true,
47 | raw: true
48 | },
49 | unknown: {
50 | expanded: true,
51 | raw: true
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Xunit Viewer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/app/visible.js:
--------------------------------------------------------------------------------
1 | import queryString from 'query-string'
2 | import { useSearchParams } from 'react-router-dom'
3 |
4 | const useVisibility = () => {
5 | const [searchParams, setSearchParams] = useSearchParams()
6 | const query = queryString.parse(searchParams.toString(), { parseBooleans: true })
7 |
8 | const passed = ('passed' in query ? query.passed : true)
9 | const skipped = ('skipped' in query ? query.skipped : true)
10 | const error = 'error' in query ? query.error : true
11 | const failure = 'failure' in query ? query.failure : true
12 | const unknown = 'unknown' in query ? query.unknown : true
13 |
14 | let fastFilter = 3
15 | if (passed && skipped && error && failure && unknown) fastFilter = 2
16 | else if (passed && skipped && !error && !failure && !unknown) fastFilter = 1
17 | else if (!passed && !skipped && error && failure && unknown) fastFilter = 0
18 |
19 | return {
20 | query: {
21 | passed,
22 | skipped,
23 | error,
24 | failure,
25 | unknown
26 | },
27 | all: passed && skipped && error && failure && unknown,
28 | fastFilter,
29 | setSearchParams
30 | }
31 | }
32 |
33 | export default useVisibility
34 |
--------------------------------------------------------------------------------
/src/cli/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import getPort from 'get-port'
3 | import HTTP from 'http'
4 | import ip from 'ip'
5 | import { Server } from 'socket.io'
6 | import getDescription from './get-description.js'
7 | import getFiles from './get-files.js'
8 | import getSuites from './get-suites.js'
9 | import render from './render.js'
10 | import watch from './watch.js'
11 |
12 | export default async (logger, args) => {
13 | const app = express()
14 | const http = HTTP.createServer(app)
15 | const io = new Server(http)
16 |
17 | app.get('/', async (req, res) => {
18 | const files = await getFiles(logger, args)
19 | const suites = await getSuites(logger, files)
20 | const description = getDescription(suites)
21 |
22 | res.send(render(logger, files, description, args, true))
23 | })
24 |
25 | io.on('connection', function (socket) {
26 | watch(args, () => {
27 | socket.emit('update', { files: getFiles(logger, args) })
28 | })
29 | })
30 |
31 | const port = await getPort({ port: args.port || 3000 })
32 | http.listen(port, function () {
33 | console.log(logger.server('Listening at', `http://${ip.address()}:${port}`))
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/src/cli/get-files.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import languageEncoding from 'detect-file-encoding-and-language'
4 |
5 | const getFiles = (logger, ignore, folder, files = []) => {
6 | if (fs.lstatSync(folder).isDirectory()) {
7 | fs.readdirSync(folder)
8 | .map(name => path.join(folder, name))
9 | .forEach(file => {
10 | if (fs.lstatSync(file).isDirectory()) files = files.concat(getFiles(logger, ignore, file, files))
11 | else if (file.endsWith('.xml') && !ignore.some(pattern => file.includes(pattern) || new RegExp(pattern).test(file))) files.push(file)
12 | else console.log(logger.warning('IGNORING:'), logger.file(file))
13 | })
14 | } else {
15 | return Array.from(new Set([folder]))
16 | }
17 | return Array.from(new Set(files))
18 | }
19 |
20 | export default async (logger, { results, ignore = [] }) => {
21 | const files = getFiles(logger, ignore, results)
22 | const readFiles = []
23 | for (const file of files) {
24 | const { encoding } = await languageEncoding(file)
25 | readFiles.push({
26 | file,
27 | contents: fs.readFileSync(file).toString((encoding || 'utf8').toLowerCase().replace(/-/g, ''))
28 | })
29 | }
30 |
31 | return readFiles
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/__snapshots__/properties-options.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders properties options 1`] = `
4 |
7 |
10 |
24 |
51 |
52 |
55 |
56 | `;
57 |
--------------------------------------------------------------------------------
/src/app/__snapshots__/suite-options.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders suite options 1`] = `
4 |
7 |
10 |
24 |
51 |
52 |
55 |
56 | `;
57 |
--------------------------------------------------------------------------------
/data/multi_cases.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | java.lang.RuntimeException: There was an error
14 | java.lang.RuntimeException: There was an error
15 |
16 |
17 | FILENAME:XX
Expected
<string>: Luke
to equal
<string>: luke
18 |
19 |
20 |
21 | This is a message
22 | This is a message
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/app/parse-all.js:
--------------------------------------------------------------------------------
1 | import merge from 'merge'
2 | import parse from './parse.js'
3 |
4 | export default async (dispatch, files, suites, filters = { passed: false, skipped: false, unknown: true, failure: true, error: true }) => {
5 | for (const { file, contents } of files) {
6 | try {
7 | const parsed = await parse(contents, filters)
8 | if (Object.keys(parsed.suites).length === 0) {
9 | dispatch({
10 | type: 'parse-error',
11 | payload: {
12 | file,
13 | error: 'No suites or tests detected in this file. It could be an unsupported format.'
14 | }
15 | })
16 | }
17 | suites = merge.recursive(true, suites, parsed)
18 | } catch (err) {
19 | dispatch({
20 | type: 'parse-error',
21 | payload: {
22 | file,
23 | error: err.message
24 | }
25 | })
26 | }
27 | }
28 |
29 | if ('suites' in suites && Object.keys(suites.suites).length > 0) {
30 | dispatch({
31 | type: 'parse-suites',
32 | payload: {
33 | suites: suites.suites
34 | }
35 | })
36 | } else {
37 | dispatch({
38 | type: 'parse-error',
39 | payload: {
40 | file: 'See errors',
41 | error: 'No suites or tests detected.'
42 | }
43 | })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/cli/render.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import Handlebars from 'handlebars'
4 | import LZUTF8 from 'lzutf8'
5 |
6 | import { fileURLToPath } from 'url'
7 | const __filename = fileURLToPath(import.meta.url)
8 | const __dirname = path.dirname(__filename)
9 |
10 | const staticDir = path.resolve(__dirname, './static')
11 |
12 | const getHTML = (type) => {
13 | const dir = path.join(staticDir, type)
14 | return fs.readdirSync(dir)
15 | .filter(file => file.endsWith(`.${type}`) && !file.includes('runtime'))
16 | .map(file => fs.readFileSync(path.join(dir, file)).toString())
17 | .join('\n')
18 | }
19 |
20 | export default (logger, files, description, { title = 'Xunit Viewer', brand, favicon }, useSockets = false) => {
21 | const scripts = getHTML('js')
22 | const styles = getHTML('css')
23 |
24 | const template = Handlebars.compile(fs.readFileSync(path.resolve(__dirname, 'index.html')).toString())
25 |
26 | files = files.map(({ file, contents }) => ({ file, contents: LZUTF8.compress(contents, { outputEncoding: 'Base64' }) }))
27 |
28 | return template({
29 | files: JSON.stringify(files),
30 | scripts,
31 | styles,
32 | title,
33 | icon: brand || 'https://lukejpreston.github.io/xunit-viewer/icon.png',
34 | favicon: favicon || 'https://lukejpreston.github.io/xunit-viewer/favicon.ico',
35 | brand,
36 | description,
37 | useSockets
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/data/properties_in_test_meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | [DELETED]/BasicWebViewControllerTests.swift:103
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/data/special_chars_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/cli/logger.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 |
3 | export default (noColor) => ({
4 | server: (message, address) => {
5 | if (noColor) return `${message} ${address}`
6 | return `${chalk.bold(message)} ${chalk.underline.blueBright(address)}`
7 | },
8 | property: (key, values) => {
9 | if (noColor) return `${key}=${values.join(', ')}`
10 | return `${chalk.bold.cyan(key)}=${chalk.cyan(values.join(', '))}`
11 | },
12 | test: (status, name) => {
13 | let icon = '?'
14 | if (status === 'failure') icon = '✗'
15 | if (status === 'passed') icon = '✓'
16 | if (status === 'error') icon = '!'
17 | if (status === 'skipped') icon = '⊘'
18 |
19 | if (noColor) return `${icon} ${name}`
20 |
21 | if (status === 'failure') return chalk.red(`${icon} ${name}`)
22 | if (status === 'passed') return chalk.green(`${icon} ${name}`)
23 | if (status === 'error') return chalk.yellow(`${icon} ${name}`)
24 | if (status === 'skipped') return chalk.gray(`${icon} ${name}`)
25 | return chalk.blueBright(`${icon} ${name}`)
26 | },
27 | time: (time) => {
28 | if (noColor) return `time=${time}`
29 | return `${chalk.bold.cyan('time')}=${chalk.cyan(time)}`
30 | },
31 | suite: (message) => {
32 | if (noColor) return message
33 | return chalk.yellow(message)
34 | },
35 | error: (message) => {
36 | if (noColor) return message
37 | return chalk.red(message)
38 | },
39 | warning: (message) => {
40 | if (noColor) return message
41 | return chalk.yellow(message)
42 | },
43 | file: (message) => {
44 | if (noColor) return message
45 | return chalk.underline.blueBright(message)
46 | }
47 | })
48 |
--------------------------------------------------------------------------------
/src/app/__snapshots__/test-options.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders test options 1`] = `
4 |
7 |
10 |
24 |
70 |
71 |
74 |
75 | `;
76 |
--------------------------------------------------------------------------------
/xunit-viewer.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import getDescription from './src/cli/get-description.js'
4 | import getFiles from './src/cli/get-files.js'
5 | import getSuites from './src/cli/get-suites.js'
6 | import Logger from './src/cli/logger.js'
7 | import render from './src/cli/render.js'
8 | import server from './src/cli/server.js'
9 | import terminal from './src/cli/terminal.js'
10 | import watch from './src/cli/watch.js'
11 |
12 | export default async (args) => {
13 | const logger = Logger(args.noColor)
14 |
15 | const results = args.results
16 | if (!fs.existsSync(results)) {
17 | const { showHelp } = import('./src/cli/args.js')
18 | showHelp()
19 | console.log(logger.error('\n The folder/file:'), logger.file(results), logger.error('does not exist'))
20 | if (!args.script) process.exit(1)
21 | }
22 |
23 | const runXunitViewer = async () => {
24 | const files = await getFiles(logger, args)
25 | const suites = await getSuites(logger, files)
26 | const description = getDescription(suites)
27 | if (args.console) terminal(suites, logger, description, args)
28 | if (args.output !== false) {
29 | const result = render(logger, files, description, args)
30 | const outputFile = path.resolve(process.cwd(), args.output)
31 | fs.writeFileSync(outputFile, result)
32 | console.log('Written to:', logger.file(outputFile))
33 | if (!args.script && !args.server) process.exit(0)
34 | }
35 | }
36 |
37 | if (args.console || args.output !== false) await runXunitViewer()
38 | if (args.server || args.port) server(logger, args)
39 | else if (args.watch) {
40 | watch(args, async () => {
41 | await runXunitViewer()
42 | })
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/data/semi-colon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {
6 | "facts": {
7 | "testscript": {
8 | "Output": "test001 succeeded!\n"
9 | }
10 | },
11 | "changed": true,
12 | "container": {
13 | "Output": "test001 succeeded!\n"
14 | },
15 | "result_failed": false
16 | }
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {
26 | "facts": {
27 | "testscript": {
28 | "Output": "test001 succeeded!\n"
29 | }
30 | },
31 | "changed": true,
32 | "container": {
33 | "Output": "test001 succeeded!\n"
34 | },
35 | "result_failed": false
36 | }
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/app/__snapshots__/hero.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders hero 1`] = `
4 |
7 |
10 |
13 |
16 |
27 |
28 |
31 |
34 |
![]()
37 |
40 |
41 |
42 |
43 |
44 |
45 | `;
46 |
47 | exports[`renders hero with title and brand 1`] = `
48 |
51 |
54 |
57 |
60 |
71 |
72 |
75 |
78 |

83 |
86 | bacon
87 |
88 |
89 |
90 |
91 |
92 |
93 | `;
94 |
--------------------------------------------------------------------------------
/data/suite-system-out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Generated test.log (if the file is not UTF-8, then this may be unreadable):
9 |
14 | from XYZ.extractor import Extractor
15 | File "/tmp/text_models/bazel/sandbox/linux-sandbox/8/execroot/text_models/bazel-out/k8-fastbuild/bin/XYZ/e2e_tests.runfiles/text_models/XYZ/extractor.py", line 7, in
16 | from ABC.DEF.helper import join_set
17 | ModuleNotFoundError: No module named 'ABC']]>
18 |
19 |
20 | Generated test.log (if the file is not UTF-8, then this may be unreadable):
21 |
26 | from XYZ.extractor import Extractor
27 | File "/tmp/text_models/bazel/sandbox/linux-sandbox/8/execroot/text_models/bazel-out/k8-fastbuild/bin/XYZ/e2e_tests.runfiles/text_models/XYZ/extractor.py", line 7, in
28 | from ABC.DEF.helper import join_set
29 | ModuleNotFoundError: No module named 'ABC']]>
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/cli/terminal.js:
--------------------------------------------------------------------------------
1 | import clear from 'console-clear'
2 |
3 | const statusRank = [
4 | 'failure',
5 | 'error',
6 | 'passed',
7 | 'skipped',
8 | 'unknown'
9 | ]
10 |
11 | export default (output, logger, description, args) => {
12 | const { suites } = output
13 | if (args.clear) clear()
14 | Object.values(suites)
15 | .sort((left, right) => {
16 | if (left.name < right.name) return -1
17 | if (left.name > right.name) return 1
18 | return 0
19 | })
20 | .forEach(suite => {
21 | console.log('\n', logger.suite(suite.name))
22 |
23 | const hasProperties = Object.entries(suite.properties)
24 | .filter(([key]) => key !== '_visible').length > 0
25 |
26 | if (hasProperties) {
27 | Object.entries(suite.properties)
28 | .filter(([key]) => key !== '_visible')
29 | .forEach(([key, values]) => {
30 | if (!Array.isArray) values = [values]
31 | console.log(' ', logger.property(key, values))
32 | })
33 | console.log()
34 | }
35 | Object.values(suite.tests)
36 | .sort((left, right) => {
37 | let leftStatus = statusRank.indexOf(left.status)
38 | let rightStatus = statusRank.indexOf(right.status)
39 |
40 | leftStatus = leftStatus === -1 ? statusRank.length : leftStatus
41 | rightStatus = rightStatus === -1 ? statusRank.length : rightStatus
42 |
43 | if (leftStatus < rightStatus) {
44 | return -2
45 | }
46 | if (leftStatus > rightStatus) return 2
47 |
48 | const leftName = left.name
49 | const rightName = right.name
50 |
51 | if (leftName < rightName) return -1
52 | if (leftName > rightName) return 1
53 |
54 | return 0
55 | })
56 | .forEach((test) => {
57 | logger.time(test.time)
58 | console.log(' ', logger.test(test.status || 'unknown', test.name), test.time ? logger.time(test.time) : '')
59 | if (test.messages.length > 0) {
60 | console.log(' -', test.messages.join('\n - '))
61 | }
62 | })
63 | })
64 | console.log()
65 | if (args.title) console.log(args.title)
66 | console.log(description)
67 | }
68 |
--------------------------------------------------------------------------------
/data/class_not_classname.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
11 | GuestBrowsingProphetBetsPageCest: Navigate and
12 | check test
13 | Failed asserting that two strings are equal.
14 | /usr/share/nginx/html/tests/_support/_generated/AcceptanceTesterActions.php:768
15 | /usr/share/nginx/html/tests/_support/Step/Prophet.php:50
16 | /usr/share/nginx/html/tests/acceptance/Bets/GuestBrowsingProphetBetsPageCest.php:49
17 |
18 |
19 |
24 | GuestBrowsingProphetOfferedPropheciesPageCest:
25 | Navigate and check test
26 | Element located either by name, CSS or XPath element with '#twoLevelTabsMenu' was not found.
27 | /usr/share/nginx/html/tests/_support/_generated/AcceptanceTesterActions.php:334
28 | /usr/share/nginx/html/tests/_support/AcceptanceTester.php:346
29 | /usr/share/nginx/html/tests/_support/Page/Element/UserProfileMenu.php:204
30 | /usr/share/nginx/html/tests/_support/Page/Element/UserProfileMenu.php:298
31 | /usr/share/nginx/html/tests/acceptance/Prophecies/GuestBrowsingProphetOfferedPropheciesPageCest.php:54
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/app/suite-options.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Toggle from './toggle.js'
3 |
4 | const Search = ({ label, dispatch }) =>
5 |
6 | {
8 | dispatch({
9 | type: 'search-suites',
10 | payload: {
11 | value: evt.target.value
12 | }
13 | })
14 | }}
15 | className='input'
16 | type='text'
17 | placeholder={label} />
18 |
19 |
20 |
21 | const Total = ({ count, total }) =>
22 | {count}/{total}
23 |
24 |
25 | const ChevronUpIcon = () =>
26 |
27 |
28 |
29 | const ChevronDownIcon = () =>
30 |
31 |
32 |
33 | const HideIcon = () =>
34 |
35 |
36 |
37 | const ShowIcon = () =>
38 |
39 |
40 |
41 | const SuiteOptions = ({ suitesExpanded = true, suitesEmpty = true, count = 0, total = 0, dispatch, active = false }) => {
42 | return
43 |
44 |
45 |
55 |
56 |
57 | {active
58 | ? <>
59 | dispatch({ type: 'toggle-all-suites' })}
61 | active={suitesExpanded}
62 | onLabel='Expanded'
63 | offLabel='Contracted'
64 | offIcon={}
65 | onIcon={} />
66 | dispatch({ type: 'toggle-empty-suites' })}
68 | active={suitesEmpty}
69 | onLabel='Empty hidden'
70 | offLabel='Empty shown'
71 | onIcon={}
72 | offIcon={} />
73 | >
74 | : null}
75 |
76 |
77 |
78 | }
79 |
80 | export default SuiteOptions
81 |
--------------------------------------------------------------------------------
/data/test-system-out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | cwd: /tmp/tmpyn7k46sd emanate --source /tmp/tmpwz2h7glq/src '/tmp/tmpwz2h7glq/src/foo' -> '/tmp/tmpwz2h7glq/dest/foo' cwd: /tmp/tmpetkvkfdw emanate --source src '/tmp/tmpetkvkfdw/src/foo' -> '/tmp/tmpetkvkfdw/dest/foo' cwd: /tmp/tmpq7vws4le/src emanate '/tmp/tmpq7vws4le/src/foo' -> '/tmp/tmpq7vws4le/dest/foo'
8 |
9 |
10 |
11 |
12 | cwd: /tmp/tmpuv0uzm7s emanate --source /tmp/tmp_uh8tzdz/src --dest /tmp/tmp_uh8tzdz/dest '/tmp/tmp_uh8tzdz/src/foo' -> '/tmp/tmp_uh8tzdz/dest/foo' '/tmp/tmp_uh8tzdz/src/bar/baz' -> '/tmp/tmp_uh8tzdz/dest/bar/baz' cwd: /tmp/tmpk99ai9r0 emanate --source src --dest /tmp/tmpk99ai9r0/dest '/tmp/tmpk99ai9r0/src/foo' -> '/tmp/tmpk99ai9r0/dest/foo' '/tmp/tmpk99ai9r0/src/bar/baz' -> '/tmp/tmpk99ai9r0/dest/bar/baz' cwd: /tmp/tmprxyuze4g/src emanate --dest /tmp/tmprxyuze4g/dest '/tmp/tmprxyuze4g/src/foo' -> '/tmp/tmprxyuze4g/dest/foo' '/tmp/tmprxyuze4g/src/bar/baz' -> '/tmp/tmprxyuze4g/dest/bar/baz'
13 |
14 |
15 |
16 |
17 | cwd: /tmp/tmp2gjl2wj_ emanate --source /tmp/tmp1t7j_ote/src --dest /tmp/tmp1t7j_ote/dest '/tmp/tmp1t7j_ote/src/foo' -> '/tmp/tmp1t7j_ote/dest/foo' cwd: /tmp/tmp9b40pw3b emanate --source src --dest /tmp/tmp9b40pw3b/dest '/tmp/tmp9b40pw3b/src/foo' -> '/tmp/tmp9b40pw3b/dest/foo' cwd: /tmp/tmpfmnr88qv/src emanate --dest /tmp/tmpfmnr88qv/dest '/tmp/tmpfmnr88qv/src/foo' -> '/tmp/tmpfmnr88qv/dest/foo'
18 |
19 |
20 |
21 |
22 | cwd: /tmp/tmp9q8tpxi2 emanate --source /tmp/tmpn4lvoqq7/src clean '/tmp/tmpn4lvoqq7/dest/foo' cwd: /tmp/tmp0dzkb_z5 emanate --source src clean '/tmp/tmp0dzkb_z5/dest/foo' cwd: /tmp/tmpsueno4qa/src emanate clean '/tmp/tmpsueno4qa/dest/foo'
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/data/multi-name-unique-classname.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | at Obi.Backend.Tests.CuentaTest.DeberiaInvocarASoaYDevolverHttpOk() in /Users/nicopaez/Projects/esfera/supervielle/obi/bff-obi/Obi.Backend.Tests/CuentaTest.cs:line 27
13 | at NUnit.Framework.Internal.TaskAwaitAdapter.GenericAdapter`1.BlockUntilCompleted()
14 | at NUnit.Framework.Internal.MessagePumpStrategy.NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaitable)
15 | at NUnit.Framework.Internal.AsyncToSyncAdapter.Await(Func`1 invoke)
16 | at NUnit.Framework.Internal.Commands.TestMethodCommand.RunTestMethod(TestExecutionContext context)
17 | at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute(TestExecutionContext context)
18 | at NUnit.Framework.Internal.Execution.SimpleWorkItem.PerformWork()
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/data/duplicate_name_unique_classanme.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | at Obi.Backend.Tests.CuentaTest.DeberiaInvocarASoaYDevolverHttpOk() in /Users/nicopaez/Projects/esfera/supervielle/obi/bff-obi/Obi.Backend.Tests/CuentaTest.cs:line 27
13 | at NUnit.Framework.Internal.TaskAwaitAdapter.GenericAdapter`1.BlockUntilCompleted()
14 | at NUnit.Framework.Internal.MessagePumpStrategy.NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaitable)
15 | at NUnit.Framework.Internal.AsyncToSyncAdapter.Await(Func`1 invoke)
16 | at NUnit.Framework.Internal.Commands.TestMethodCommand.RunTestMethod(TestExecutionContext context)
17 | at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute(TestExecutionContext context)
18 | at NUnit.Framework.Internal.Execution.SimpleWorkItem.PerformWork()
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/data/multi_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
14 |
15 |
20 |
21 |
26 |
27 |
32 |
33 |
38 |
39 |
44 |
45 |
46 |
52 |
57 |
58 |
63 |
64 |
69 |
70 |
75 |
76 |
81 |
82 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/cli/static/js/main.4e6e0818.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2023 Fonticons, Inc.
5 | */
6 |
7 | /*!
8 | * The buffer module from node.js, for the browser.
9 | *
10 | * @author Feross Aboukhadijeh
11 | * @license MIT
12 | */
13 |
14 | /*!
15 | LZ-UTF8 v0.6.3
16 |
17 | Copyright (c) 2021, Rotem Dan
18 | Released under the MIT license.
19 |
20 | Build date: 2022-07-06
21 |
22 | Please report any issue at https://github.com/rotemdan/lzutf8.js/issues
23 | */
24 |
25 | /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
26 |
27 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */
28 |
29 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
30 |
31 | /**
32 | * @license React
33 | * react-dom.production.min.js
34 | *
35 | * Copyright (c) Facebook, Inc. and its affiliates.
36 | *
37 | * This source code is licensed under the MIT license found in the
38 | * LICENSE file in the root directory of this source tree.
39 | */
40 |
41 | /**
42 | * @license React
43 | * react-jsx-runtime.production.min.js
44 | *
45 | * Copyright (c) Facebook, Inc. and its affiliates.
46 | *
47 | * This source code is licensed under the MIT license found in the
48 | * LICENSE file in the root directory of this source tree.
49 | */
50 |
51 | /**
52 | * @license React
53 | * react.production.min.js
54 | *
55 | * Copyright (c) Facebook, Inc. and its affiliates.
56 | *
57 | * This source code is licensed under the MIT license found in the
58 | * LICENSE file in the root directory of this source tree.
59 | */
60 |
61 | /**
62 | * @license React
63 | * scheduler.production.min.js
64 | *
65 | * Copyright (c) Facebook, Inc. and its affiliates.
66 | *
67 | * This source code is licensed under the MIT license found in the
68 | * LICENSE file in the root directory of this source tree.
69 | */
70 |
71 | /**
72 | * @remix-run/router v1.6.3
73 | *
74 | * Copyright (c) Remix Software Inc.
75 | *
76 | * This source code is licensed under the MIT license found in the
77 | * LICENSE.md file in the root directory of this source tree.
78 | *
79 | * @license MIT
80 | */
81 |
82 | /**
83 | * React Router DOM v6.13.0
84 | *
85 | * Copyright (c) Remix Software Inc.
86 | *
87 | * This source code is licensed under the MIT license found in the
88 | * LICENSE.md file in the root directory of this source tree.
89 | *
90 | * @license MIT
91 | */
92 |
93 | /**
94 | * React Router v6.13.0
95 | *
96 | * Copyright (c) Remix Software Inc.
97 | *
98 | * This source code is licensed under the MIT license found in the
99 | * LICENSE.md file in the root directory of this source tree.
100 | *
101 | * @license MIT
102 | */
103 |
--------------------------------------------------------------------------------
/src/cli/get-files.test.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import getFiles from './get-files'
3 |
4 | const expectedFiles = [
5 | path.resolve(__dirname, '../../data/class_not_classname.xml'),
6 | path.resolve(__dirname, '../../data/complete_no_suite.xml'),
7 | path.resolve(__dirname, '../../data/complete_no_suite_multi_cases.xml'),
8 | path.resolve(__dirname, '../../data/complete_no_suite_single_suite.xml'),
9 | path.resolve(__dirname, '../../data/complete_single_case_only.xml'),
10 | path.resolve(__dirname, '../../data/complete_single_suite.xml'),
11 | path.resolve(__dirname, '../../data/defect_suite.xml'),
12 | path.resolve(__dirname, '../../data/duplicate_name_unique_classanme.xml'),
13 | path.resolve(__dirname, '../../data/embedded_html_sysout.xml'),
14 | path.resolve(__dirname, '../../data/error_suite.xml'),
15 | path.resolve(__dirname, '../../data/failing_suite.xml'),
16 | path.resolve(__dirname, '../../data/invalid.xml'),
17 | path.resolve(__dirname, '../../data/issue_2.xml'),
18 | path.resolve(__dirname, '../../data/issue_3.xml'),
19 | path.resolve(__dirname, '../../data/lots-of-results.xml'),
20 | path.resolve(__dirname, '../../data/malformed.xml'),
21 | path.resolve(__dirname, '../../data/most_complex.xml'),
22 | path.resolve(__dirname, '../../data/multi-name-unique-classname.xml'),
23 | path.resolve(__dirname, '../../data/multi_cases.xml'),
24 | path.resolve(__dirname, '../../data/multi_error_test_with_system_out.xml'),
25 | path.resolve(__dirname, '../../data/multi_suite.xml'),
26 | path.resolve(__dirname, '../../data/name.has.dots.xml'),
27 | path.resolve(__dirname, '../../data/nested-nested.xml'),
28 | path.resolve(__dirname, '../../data/no_class_name.xml'),
29 | path.resolve(__dirname, '../../data/passing_suite.xml'),
30 | path.resolve(__dirname, '../../data/properties_in_test_meta.xml'),
31 | path.resolve(__dirname, '../../data/pytest_testcase_properties.xml'),
32 | path.resolve(__dirname, '../../data/russian-unicode.xml'),
33 | path.resolve(__dirname, '../../data/semi-colon.xml'),
34 | path.resolve(__dirname, '../../data/skipped_suite.xml'),
35 | path.resolve(__dirname, '../../data/special_chars_suite.xml'),
36 | path.resolve(__dirname, '../../data/suite-system-out.xml'),
37 | path.resolve(__dirname, '../../data/test-system-out.xml'),
38 | path.resolve(__dirname, '../../data/test.xml'),
39 | path.resolve(__dirname, '../../data/utf-16.xml'),
40 | path.resolve(__dirname, '../../data/with_html.xml'),
41 | path.resolve(__dirname, '../../data/xunit-2-2.xml'),
42 | path.resolve(__dirname, '../../data/xunit-2.xml')
43 | ]
44 |
45 | test('get files', async () => {
46 | const files = await getFiles({
47 | warning: (input) => input,
48 | file: (input) => input
49 | }, { results: path.resolve(__dirname, '../../data'), ignore: ['_thingy'] })
50 | expect(files.map(({ file }) => file)).toEqual(expectedFiles)
51 | expect(files.map(({ file }) => file)).toEqual(expect.not.arrayContaining([path.resolve(__dirname, '../../data/subfolder/_thingy.xml')]))
52 | expect(files.filter(({ contents }) => contents === '').length).toBe(0)
53 | })
54 |
--------------------------------------------------------------------------------
/data/complete_no_suite_single_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | inner massage
19 |
20 |
21 | inner massage
22 |
23 |
24 | inner massage
25 |
26 |
27 | inner massage
28 |
29 |
30 | inner message
31 |
32 |
33 |
34 | inner message
35 |
36 |
37 |
38 |
39 | inner massage
40 | inner massage
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | <i>inner</i><b>message</b>
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/app/properties-options.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Toggle from './toggle.js'
3 |
4 | const Search = ({ label, dispatch }) =>
5 |
6 | {
8 | dispatch({
9 | type: 'search-properties',
10 | payload: {
11 | value: evt.target.value
12 | }
13 | })
14 | }}
15 | className='input'
16 | type='text'
17 | placeholder={label} />
18 |
19 |
20 |
21 | const Total = ({ count, total }) =>
22 | {count}/{total}
23 |
24 |
25 | const EyeIcon = () =>
26 |
27 |
28 |
29 | const EyeSlashIcon = () =>
30 |
31 |
32 |
33 | const ChevronUpIcon = () =>
34 |
35 |
36 |
37 | const ChevronDownIcon = () =>
38 |
39 |
40 |
41 | const ToggleRow = ({ type, label, propertiesVisible, propertiesExpanded, dispatch }) =>
42 |
43 | {label}
44 |
45 |
{
49 | dispatch({
50 | type: 'toggle-properties-visbility',
51 | payload: {
52 | type,
53 | active: !propertiesVisible[type]
54 | }
55 | })
56 | }}
57 | onLabel='Visible'
58 | offLabel='Hidden'
59 | onIcon={}
60 | offIcon={} />
61 | {
63 | dispatch({
64 | type: 'toggle-all-properties',
65 | payload: {
66 | type,
67 | active: !propertiesExpanded[type]
68 | }
69 | })
70 | }}
71 | className='properties-options-toggle'
72 | active={propertiesExpanded[type]}
73 | onLabel='Expanded'
74 | offLabel='Contracted'
75 | offIcon={}
76 | onIcon={} />
77 |
78 |
79 | const PropertiesOptions = ({ count = 0, total = 0, active = false, dispatch, propertiesExpanded = { all: true, suites: true, tests: true }, propertiesVisible = { all: true, suites: true, tests: true } }) => {
80 | return
81 |
82 |
83 |
93 |
94 |
95 | {active
96 | ?
97 |
98 |
99 |
100 |
101 | : null}
102 |
103 |
104 |
105 | }
106 |
107 | export default PropertiesOptions
108 |
--------------------------------------------------------------------------------
/src/app/files.js:
--------------------------------------------------------------------------------
1 | // import React from 'react'
2 | // import { UnControlled as CodeMirror } from 'react-codemirror2'
3 | // import 'codemirror/lib/codemirror.css'
4 | // import 'codemirror/mode/xml/xml'
5 |
6 | // const ToggleFiles = ({ onClick }) =>
17 |
18 | // const Files = ({ active = false, setActive, files = [] }) => {
19 | // return
20 | //
21 | // { setActive(!active) }} />
22 | //
23 | //
24 | //
72 |
73 | //
74 | // {active
75 | // ?
77 | //
78 | //
79 | // java.lang.RuntimeException: There was an error
80 | //
81 | // `}
82 | // options={{
83 | // mode: 'xml',
84 | // lineNumbers: true
85 | // }}
86 | // onChange={(cm, { text }, value) => {
87 |
88 | // }}
89 | // />
90 | // : null}
91 | //
92 |
93 | //
94 | // }
95 |
96 | // export default Files
97 |
98 | const Files = () => null
99 |
100 | export default Files
101 |
--------------------------------------------------------------------------------
/data/most_complex.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | java.lang.RuntimeException: There was an error
18 |
19 |
20 | <i>WITH</i><b>HTML</b>
21 |
22 |
23 | For some reason a passing message
24 |
25 |
26 | This message has a link github.com/lukejpreston/xunit-viewer and an email example@gmail.com
27 |
28 |
29 | java.lang.RuntimeException: There was an error 1
30 | java.lang.RuntimeException: There was an error 2
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xunit-viewer",
3 | "type": "module",
4 | "version": "10.6.1",
5 | "description": "parses xunit xml into xunit viewer",
6 | "repository": {
7 | "url": "https://github.com/lukejpreston/xunit-viewer.git",
8 | "type": "git"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/lukejpreston/xunit-viewer.git"
12 | },
13 | "homepage": "./",
14 | "keywords": [
15 | "test",
16 | "junit",
17 | "xunit",
18 | "viewer",
19 | "parser"
20 | ],
21 | "author": "lukejpreston ",
22 | "license": "MIT",
23 | "main": "xunit-viewer.js",
24 | "bin": {
25 | "xunit-viewer": "bin/xunit-viewer.js"
26 | },
27 | "directories": {
28 | "bin": "bin",
29 | "cli": "src/cli"
30 | },
31 | "scripts": {
32 | "start": "react-scripts start",
33 | "build": "react-scripts build",
34 | "test": "react-scripts test --reporters=default --reporters=jest-junit",
35 | "update": "node src/cli/update-expected.js",
36 | "eject": "react-scripts eject",
37 | "lint": "eslint xunit-viewer.js src --ignore-pattern src/cli/static/js/**/*",
38 | "test:ci": "echo blah",
39 | "test:document": "./bin/xunit-viewer.js -r junit.xml -o gh-pages/xunit-viewer-results.html",
40 | "test:document:serve": "npm run test:document -- -s",
41 | "demo": "./bin/xunit-viewer.js -r data -o gh-pages/index.html",
42 | "release:demo": "npm run test:document && npm run demo && gh-pages -d gh-pages",
43 | "deploy": "rm -rf src/cli/static && cp -r build/static src/cli/static",
44 | "build:cli": "npm run build && npm run deploy",
45 | "release": "./release.sh"
46 | },
47 | "dependencies": {
48 | "@uiw/react-codemirror": "^4.21.3",
49 | "chalk": "^5.2.0",
50 | "chokidar": "^3.5.3",
51 | "console-clear": "^1.1.1",
52 | "debounce": "^1.2.1",
53 | "detect-file-encoding-and-language": "^2.4.0",
54 | "express": "^4.18.2",
55 | "get-port": "^7.0.0",
56 | "handlebars": "^4.7.7",
57 | "ip": "^1.1.8",
58 | "lzutf8": "^0.6.3",
59 | "merge": "^2.1.1",
60 | "socket.io": "^4.6.2",
61 | "xml2js": "^0.6.0",
62 | "yargs": "^17.7.2"
63 | },
64 | "devDependencies": {
65 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
66 | "@fortawesome/fontawesome-free": "^6.4.0",
67 | "bulma": "^0.9.4",
68 | "eslint": "^8.43.0",
69 | "eslint-config-standard": "^17.1.0",
70 | "eslint-config-standard-react": "^13.0.0",
71 | "eslint-import-resolver-node": "^0.3.7",
72 | "eslint-plugin-import": "^2.27.5",
73 | "eslint-plugin-node": "^11.1.0",
74 | "eslint-plugin-promise": "^6.1.1",
75 | "eslint-plugin-react": "^7.32.2",
76 | "eslint-plugin-standard": "^5.0.0",
77 | "fuzzy": "^0.1.3",
78 | "gh-pages": "^5.0.0",
79 | "jest-junit": "^16.0.0",
80 | "linkify-html": "^4.1.1",
81 | "linkifyjs": "^4.1.1",
82 | "localforage": "^1.10.0",
83 | "match-sorter": "^6.3.1",
84 | "query-string": "^8.1.0",
85 | "react": "^18.2.0",
86 | "react-dom": "^18.2.0",
87 | "react-linkify": "^1.0.0-alpha",
88 | "react-render-if-visible": "^2.1.1",
89 | "react-router-dom": "^6.13.0",
90 | "react-scripts": "^5.0.1",
91 | "react-test-renderer": "^18.2.0",
92 | "sort-by": "^1.2.0",
93 | "stream": "^0.0.2",
94 | "timers": "^0.1.1"
95 | },
96 | "browserslist": [
97 | ">0.2%",
98 | "not dead",
99 | "not ie <= 11",
100 | "not op_mini all"
101 | ],
102 | "eslintIgnore": [
103 | "cli/static/**/*"
104 | ],
105 | "eslintConfig": {
106 | "extends": [
107 | "standard",
108 | "standard-react",
109 | "plugin:react/recommended"
110 | ],
111 | "env": {
112 | "browser": true,
113 | "jest": true,
114 | "jasmine": true
115 | },
116 | "rules": {
117 | "react/prop-types": 0,
118 | "react/jsx-closing-tag-location": 0,
119 | "react/jsx-closing-bracket-location": 0
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/data/test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | value 2
10 |
11 |
12 | value with no name
13 |
14 |
15 |
16 |
17 |
18 | value only
19 |
20 | only a message
21 |
22 |
23 |
24 |
25 |
26 | inner message
27 |
28 | HERE IS SOME TEXT
29 | inner massage 1
30 |
31 |
32 | inner massage 1
33 | inner massage 2
34 |
35 |
36 | inner massage 1
37 | inner massage 2
38 |
39 |
40 | inner massage 1
41 | inner massage 2
42 |
43 |
44 | inner massage 1
45 | inner massage 2
46 |
47 |
48 | error inner massage 1
49 | failure inner massage 2
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | value 2
77 |
78 |
79 | value with no name
80 |
81 |
82 |
83 |
84 |
85 | value only
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/data/complete_no_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | inner massage
19 |
20 |
21 | inner massage
22 |
23 |
24 | inner massage
25 |
26 |
27 | inner massage
28 |
29 |
30 | inner message
31 |
32 |
33 |
34 | inner message
35 |
36 |
37 |
38 |
39 | inner massage
40 | inner massage
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | <i>inner</i><b>message</b>
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/cli/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{title}}
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
95 | {{#if useSockets}}
96 |
97 | {{/if}}
98 |
103 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/data/embedded_html_sysout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 | ]]>
9 |
10 |
11 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/cli/args.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import yargs from 'yargs/yargs'
3 | import { hideBin } from 'yargs/helpers'
4 |
5 | const instance = yargs(hideBin(process.argv))
6 | .command('xunit-viewer', 'Renders Xunit style xml results')
7 | .example('xunit-viewer -r file.xml', 'a file')
8 | .example('xunit-viewer -r folder', 'a folder')
9 | .example('xunit-viewer -r folder -i *-broke.xml', 'ignore')
10 | .example('xunit-viewer -r folder -o my-tests.html', 'rename output')
11 | .example('xunit-viewer -r folder -t "My Tests"', 'change HTML title')
12 | .example('xunit-viewer -r folder -b https://image.png', 'change the image')
13 | .example('xunit-viewer -r folder -f https://image.favico', 'change the favicon')
14 | .example('xunit-viewer -r folder -c', 'render in console')
15 | .example('xunit-viewer -r folder -c -C', 'render in console, do not clear')
16 | .example('xunit-viewer -r folder -c -n', 'no color in console')
17 | .example('xunit-viewer -r folder -w', 'start watch')
18 | .example('xunit-viewer -r folder -w -p 5050', 'watch at 5050')
19 | // .example('xunit-viewer -r folder --s.s "value"', 'search suite with term "value"')
20 |
21 | .string('results')
22 | .coerce('results', (arg) => path.resolve(process.cwd(), arg))
23 | .alias('r', 'results')
24 | .describe('r', 'File/Folder of results')
25 | .demandOption(['results'])
26 |
27 | .array('ignore')
28 | .alias('i', 'ignore')
29 | .describe('i', 'Ignore patterns')
30 |
31 | .string('output')
32 | .default('output', 'index.html')
33 | .coerce('output', (arg) => {
34 | if (arg === 'false') return false
35 | return path.resolve(process.cwd(), arg.endsWith('.html') ? arg : `${arg}.html`)
36 | })
37 | .alias('o', 'output')
38 | .describe('o', 'Output filename')
39 |
40 | .string('title')
41 | .alias('t', 'title')
42 | .describe('t', 'HTML title e.g. "My Tests"')
43 |
44 | .string('brand')
45 | .alias('b', 'brand')
46 | .describe('b', 'Provide a URL with your own logo')
47 |
48 | .string('favicon')
49 | .alias('f', 'favicon')
50 | .describe('f', 'Provide a URL with your own favicon')
51 |
52 | .boolean('console')
53 | .alias('c', 'console')
54 | .describe('c', 'Render in console')
55 |
56 | .boolean('clear')
57 | .alias('C', 'clear')
58 | .default('C', true)
59 | .describe('C', 'Clears the console')
60 |
61 | .boolean('server')
62 | .default('s', false)
63 | .alias('s', 'server')
64 | .describe('s', 'Start a server and sockets for live updates')
65 |
66 | .boolean('no-color')
67 | .alias('n', 'no-color')
68 | .describe('n', 'No color in the console')
69 |
70 | .boolean('watch')
71 | .alias('w', 'watch')
72 | .describe('w', 'Re-run when a file changes')
73 |
74 | .number('port')
75 | .alias('p', 'port')
76 | .describe('p', 'Starts a server with sockets on that port, if no port is provided then it will run on port 3000 (or next available)')
77 |
78 | // .string('properties.search')
79 | // .alias('p.s', 'properties.search')
80 | // .describe('p.s', 'pre-filter option')
81 |
82 | // .boolean('properties.visible')
83 | // .alias('p.v', 'properties.visible')
84 | // .describe('p.v', 'pre-filter option')
85 |
86 | // .string('suites.search')
87 | // .alias('s.s', 'suites.search')
88 | // .describe('s.s', 'pre-filter option')
89 |
90 | // const types = ['tests']
91 | // const statuses = ['passed', 'failure', 'skipped', 'error', 'unknown']
92 | // const actions = ['visible']
93 |
94 | // types.forEach(type => {
95 | // const firstTypeChar = type[0]
96 | // const searchCommand = `${type}.search`
97 | // const searchAlias = `${firstTypeChar}.s`
98 |
99 | // instance.string(searchCommand)
100 | // .alias(searchAlias, searchCommand)
101 | // .describe(searchAlias, 'pre-filter option')
102 |
103 | // statuses.forEach(status => {
104 | // const firstStatusChar = status[0]
105 | // actions.forEach(action => {
106 | // const firstActionChar = action[0]
107 | // const toggleCommand = `${type}.${status}.${action}`
108 | // const toggleAlias = `${firstTypeChar}.${firstStatusChar}.${firstActionChar}`
109 |
110 | // instance.boolean(toggleCommand)
111 | // .alias(toggleAlias, toggleCommand)
112 | // .describe(toggleAlias, 'pre-filter option')
113 | // })
114 | // })
115 | // })
116 |
117 | instance.help()
118 |
119 | export const args = instance.argv
120 | export const showHelp = instance.showHelp
121 |
--------------------------------------------------------------------------------
/data/complete_single_suite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | inner massage
20 |
21 |
22 | inner massage
23 |
24 |
25 | inner massage
26 |
27 |
28 | inner massage
29 |
30 |
31 | inner message
32 |
33 |
34 |
35 | inner message
36 |
37 |
38 |
39 |
40 | inner massage
41 | inner massage
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | <i>inner</i><b>message</b>
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | inner massage
71 |
72 |
73 |
74 |
75 |
76 | inner massage
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import '@fortawesome/fontawesome-free/js/all.js'
2 | import 'bulma/css/bulma.css'
3 | import React from 'react'
4 | import ReactDOM from 'react-dom/client'
5 | import {
6 | createBrowserRouter,
7 | RouterProvider
8 | } from 'react-router-dom'
9 |
10 | import App from './app/app.js'
11 | import './app/index.css'
12 |
13 | import LZUTF8 from 'lzutf8'
14 |
15 | let files = window.files || []
16 | const title = window.title || 'Xunit Viewer'
17 | const brand = window.brand || null
18 |
19 | if (process.env.NODE_ENV === 'development') {
20 | files = [{
21 | file: '/path/to/file/complete.xml',
22 | contents: LZUTF8.compress(`
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | java.lang.RuntimeException: There was an error
40 |
41 |
42 | <i>WITH</i><b>HTML</b>
43 |
44 |
45 | For some reason a passing message
46 |
47 |
48 | This message has a link github.com/lukejpreston/xunit-viewer and an email example@gmail.com
49 |
50 |
51 | java.lang.RuntimeException: There was an error 1
52 | java.lang.RuntimeException: There was an error 2
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | `, { outputEncoding: 'Base64' })
84 | }]
85 | }
86 |
87 | files = files.map(({ file, contents }) => ({
88 | file,
89 | contents: LZUTF8.decompress(contents, { inputEncoding: 'Base64' })
90 | }))
91 |
92 | const router = createBrowserRouter([
93 | {
94 | path: '/',
95 | element: ,
96 | errorElement:
97 | }
98 | ])
99 |
100 | ReactDOM
101 | .createRoot(document.getElementById('root'))
102 | .render(
103 |
104 |
105 |
106 | )
107 |
--------------------------------------------------------------------------------
/src/app/app.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useReducer } from 'react'
2 | // import Files from './files'
3 | import Error from './error.js'
4 | import Hero from './hero.js'
5 | import initialState from './initial-state.js'
6 | import Loading from './loading.js'
7 | import parseAll from './parse-all.js'
8 | import PropertiesOptions from './properties-options.js'
9 | import reducer from './reducer.js'
10 | import SuiteOptions from './suite-options.js'
11 | import Suite from './suite.js'
12 | import TestOptions from './test-options.js'
13 |
14 | const App = ({ files, title, brand }) => {
15 | const [state, dispatch] = useReducer(reducer, initialState)
16 |
17 | useEffect(() => {
18 | if (Object.keys(state.suites).length === 0) parseAll(dispatch, files)
19 | // eslint-disable-next-line react-hooks/exhaustive-deps
20 | }, [])
21 |
22 | useEffect(() => {
23 | window.onbeforeprint = () => {
24 | dispatch({ type: 'print-mode', payload: { printMode: true } })
25 | }
26 |
27 | window.onafterprint = () => {
28 | dispatch({ type: 'print-mode', payload: { printMode: false } })
29 | }
30 | }, [])
31 |
32 | let currentPropertiesCount = 0
33 | let propertiesTotal = 0
34 | Object.entries(state.currentSuites).forEach(([key, suite]) => {
35 | currentPropertiesCount += Object.keys(suite.properties).filter(key => key !== '_active' && key !== '_visible').length
36 | Object.values(suite.tests).forEach(test => {
37 | if (test.properties) currentPropertiesCount += Object.keys(test.properties).filter(key => key !== '_active' && key !== '_visible').length
38 | })
39 | })
40 | Object.entries(state.currentSuites).forEach(([key, suite]) => {
41 | propertiesTotal += Object.keys(suite.properties).filter(key => key !== '_active' && key !== '_visible').length
42 | Object.values(suite.tests).forEach(test => {
43 | if (test.properties) propertiesTotal += Object.keys(test.properties).filter(key => key !== '_active' && key !== '_visible').length
44 | })
45 | })
46 |
47 | const testCounts = {}
48 | let testCount = 0
49 | let testTotal = 0
50 | Object.entries(state.currentSuites).forEach(([key, suite]) => {
51 | Object.entries(suite.tests).forEach(([key, test]) => {
52 | const status = test.status || 'unknown'
53 | testCounts[status] = testCounts[status] || {}
54 | testCounts[status].count = testCounts[status].count || 0
55 | testCounts[status].total = testCounts[status].total || 0
56 |
57 | testCounts[status].count += 1
58 | testCounts[status].total += 1
59 |
60 | testTotal += 1
61 | testCount += 1
62 | })
63 | })
64 |
65 | const onUpdate = ({ files }) => {
66 | parseAll(dispatch, files, {})
67 | }
68 |
69 | window.sockets = window.sockets || null
70 | useEffect(() => {
71 | if (window.sockets === null && 'io' in window) {
72 | window.sockets = window.io()
73 | window.sockets.on('update', onUpdate)
74 | }
75 | })
76 |
77 | return
78 |
{ dispatch({ type: 'toggle-menu' }) }}
81 | title={title}
82 | brand={brand}
83 | printMode={state.printMode}
84 | burger={state.hero.burger}
85 | dropdown={state.hero.dropdown}
86 | dispatch={dispatch}
87 | suites={state.currentSuites}
88 | />
89 |
117 |
118 |
119 | {state.errors &&
}
120 | {state.errors === null && Object.values(state.currentSuites).length === 0 &&
}
121 | {Object.values(state.currentSuites).length > 0 &&
122 | {
123 | Object.values(state.currentSuites)
124 | .sort((left, right) => {
125 | if (left.name < right.name) return -1
126 | if (left.name > right.name) return 1
127 | return 0
128 | })
129 | .map(suite => (
130 |
137 | ))
138 | }
139 |
140 | }
141 |
142 |
143 |
144 | }
145 |
146 | export default App
147 |
--------------------------------------------------------------------------------
/src/app/test-options.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Toggle from './toggle.js'
3 | import useVisibility from './visible.js'
4 |
5 | const icons = {
6 | passed: 'check',
7 | failure: 'times',
8 | error: 'exclamation',
9 | skipped: 'ban',
10 | unknown: 'question'
11 | }
12 |
13 | const Search = ({ label, dispatch, suite, id }) =>
14 |
15 | {
17 | dispatch({
18 | type: 'search-tests',
19 | payload: {
20 | value: evt.target.value
21 | }
22 | })
23 | }}
24 | className='input'
25 | type='text'
26 | placeholder={label} />
27 |
28 |
29 |
30 | const Total = ({ count, total, icon }) =>
31 | {icon
32 | ?
33 |
34 |
35 | : null}
36 | {count}/{total}
37 |
38 |
39 | const fromTestCounts = (testCounts, status, value) => {
40 | const statusCounts = testCounts[status] || {}
41 | return statusCounts[value] || 0
42 | }
43 |
44 | const EyeIcon = () => <>
45 |
46 |
47 |
48 | >
49 |
50 | const EyeSlashIcon = () => <>
51 |
52 |
53 |
54 | >
55 |
56 | const PrettyIcon = () =>
57 |
58 |
59 |
60 | const CodeIcon = () =>
61 |
62 |
63 |
64 | const ChevronUpIcon = () =>
65 |
66 |
67 |
68 | const ChevronDownIcon = () =>
69 |
70 |
71 |
72 | const StatusTotal = ({ testCounts, status }) => {
73 | return fromTestCounts(testCounts, status, 'total') > 0 ? : null
74 | }
75 |
76 | const ToggleRow = ({ status, label, dispatch, visible = true, expanded = true, raw = true }) => {
77 | const { query, setSearchParams } = useVisibility()
78 | return
79 |
80 | {status !== 'all'
81 | ?
82 |
83 |
84 | :
85 |
86 | }
87 | {label}
88 |
89 |
{
91 | if (status !== 'all') {
92 | setSearchParams({
93 | ...query,
94 | [status]: !visible
95 | })
96 | } else {
97 | const anyFalse = Object.values(query).some(visible => visible === false)
98 | setSearchParams({
99 | passed: anyFalse,
100 | skipped: anyFalse,
101 | failure: anyFalse,
102 | error: anyFalse,
103 | unknown: anyFalse
104 | })
105 | }
106 | }}
107 | active={visible}
108 | onLabel='Visible'
109 | offLabel='Hidden'
110 | onIcon={}
111 | offIcon={} />
112 | {
114 | dispatch({
115 | type: 'toggle-test-expanded',
116 | payload: {
117 | status,
118 | active: !expanded
119 | }
120 | })
121 | }}
122 | active={expanded}
123 | onLabel='Expanded'
124 | offLabel='Contracted'
125 | onIcon={}
126 | offIcon={} />
127 | {
129 | dispatch({
130 | type: 'toggle-test-raw',
131 | payload: {
132 | status,
133 | active: !raw
134 | }
135 | })
136 | }}
137 | active={raw}
138 | onLabel='Raw'
139 | offLabel='Pretty'
140 | onIcon={}
141 | offIcon={} />
142 |
143 | }
144 |
145 | const Options = ({
146 | testCounts = {},
147 | testToggles = {},
148 | count = 0,
149 | total = 0,
150 | dispatch,
151 | active = false
152 | }) => {
153 | const { all, query: { passed, failure, error, skipped, unknown } } = useVisibility()
154 |
155 | return
156 |
174 |
175 | {active
176 | ? <>
177 |
178 |
179 |
180 |
181 |
182 |
183 | >
184 | : null}
185 |
186 |
187 |
188 | }
189 |
190 | export default Options
191 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Xunit Viewer
2 |
3 | Takes all your XUnit and JUnit XML files and makes them readable
4 |
5 | 
6 |
7 | [](https://badge.fury.io/js/xunit-viewer)
8 | [](https://www.npmjs.com/package/xunit-viewer)
9 | [](https://github.com/lukejpreston/xunit-viewer/actions?query=workflow%3ACI)
10 |
11 | Have a look at the [demo](https://lukejpreston.github.io/xunit-viewer/)
12 |
13 | ## Features
14 |
15 | * Generate an HTML single file with the ability to search, filter
16 | * Render results out to the console, this comes with the ability to search and filter
17 | * Re-run the above when a file changes
18 | * Start a server with WebSockets to keep the browser in sync with the data
19 | * Add files to the web app without having to re-run xunit viewer
20 | * Adds the metadata to the header so you can share the URL in places such as slack, for example
21 | * Use the query parameter to filter tests
22 |
23 | 
24 |
25 | Xunit Viewer supports node LTS version but should work on node 10+
26 |
27 | ## URL filtering
28 |
29 | You can filter by test status to save time on refreshes, and update the query params
30 |
31 | `FILE|ROUTE/?passed=true&error=false&failure=false&skipped=true&unknown=false`
32 |
33 | ## Usage, CLI
34 |
35 | ```sh
36 | npm i -g xunit-viewer
37 | xunit-viewer --help
38 | ```
39 |
40 | ### Commands
41 |
42 | ```text
43 | xunit-viewer [command]
44 |
45 | Commands:
46 | xunit-viewer Renders Xunit style xml results
47 |
48 | Options:
49 | --version Show version number [boolean]
50 | -r, --results File/Folder of results [string] [required]
51 | -i, --ignore Ignore patterns [array]
52 | -o, --output Output filename [string]
53 | -t, --title HTML title e.g. "My Tests" [string]
54 | -b, --brand Provide a URL with your own logo [string]
55 | -f, --favicon Provide a URL with your own favicon [string]
56 | -c, --console Render in console [boolean]
57 | -C, --clear Clears the console [boolean] [default: true]
58 | -s, --server Start a server and sockets for live updates
59 | [boolean] [default: false]
60 | -n, --no-color No color in the console [boolean]
61 | -w, --watch Re-run when a file changes [boolean]
62 | -p, --port Starts a server with sockets on that port, if no port is
63 | provided then it will run on port 3000 (or next available)
64 | [number]
65 | --help Show help [boolean]
66 |
67 | Examples:
68 | xunit-viewer -r file.xml a file
69 | xunit-viewer -r folder a folder
70 | xunit-viewer -r folder -i *-broke.xml ignore
71 | xunit-viewer -r folder -o my-tests.html rename output
72 | xunit-viewer -r folder -t "My Tests" change HTML title
73 | xunit-viewer -r folder -b https://image.png change the image
74 | xunit-viewer -r folder -f https://image.favico change the favicon
75 | xunit-viewer -r folder -c render in console
76 | xunit-viewer -r folder -c -s false render in console and do not save
77 | xunit-viewer -r folder -c -n no color in console
78 | xunit-viewer -r folder -w start watch
79 | xunit-viewer -r folder -w -p 5050 watch at 5050
80 | ```
81 |
82 | ## Usage, Node
83 |
84 | Xunit Viewer is asynchronous so you may need to wrap it up like so. **NOTE** The `script` parameter which will skip all Xunit Viewer's exit codes.
85 |
86 | ```js
87 | import xunitViewer from 'xunit-viewer'
88 |
89 | const main = async () => {
90 | await xunitViewer({
91 | server: false,
92 | results: 'data',
93 | ignore: ['_thingy', 'invalid'],
94 | title: 'Xunit View Sample Tests',
95 | output: 'output.html',
96 | script: true
97 | })
98 | }
99 | main()
100 | ```
101 |
102 | If you are going to run it from a script with no other code
103 |
104 | ```js
105 | import xunitViewer from 'xunit-viewer'
106 |
107 | xunitViewer({
108 | server: false,
109 | results: 'data',
110 | ignore: ['_thingy', 'invalid'],
111 | title: 'Xunit View Sample Tests',
112 | output: 'output.html',
113 | script: true
114 | })
115 | ```
116 |
117 | ## Usage, React
118 |
119 | not available
120 |
121 | ## Contributing
122 |
123 | A list of available commands
124 |
125 | ```sh
126 | npm i
127 | npm start # this starts the dev app
128 | npm release # this updates the code in the cli folder
129 | npm run demo # this generates the demo
130 | ./bin/xunit-viewer # to run the local command line tool
131 |
132 | npm test # runs the tests
133 | npm run test:ci # runs without watch and also generates a html output
134 | npm run lint # runs eslint
135 | npm run update # updates the expected files for you
136 | npm run build:cli # builds the js and copies it to the cli
137 | ```
138 |
139 | Make sure your tests are running and passing, and the linter is passing as well
140 |
141 | **DO NOT** commit the `src/cli/static` folder or the `junit.xml` file as part of your PR as these are auto-generated and just clutters up the PR, future work will be done to not make them part of the repo but they currently need to be included for the tags
142 |
143 | A suggested workflow for UI changes
144 |
145 | 1. `npm i` to install the project
146 | 2. `npm start` to start the dev application then you can make your changes quickly
147 | 3. `npm test` to run the tests, run `npm run updated` to quickly update expected values
148 | 4. `npm run lint` to make sure all the files and nice and linted
149 | 5. `npm build:cli` in order to update the CLI with your UI changes
150 | 6. `./bin/xunit-viewer -r data -o test-output.html` in order to make sure the commands work as expected
151 |
152 | If your work does not include any UI work then a suggestion is
153 |
154 | 1. `npm i` to install the project
155 | 2. `./bin/xunit-viewer ...` in order to make sure the commands work as expected
156 | 3. `npm test` to run the tests, run `npm run updated` to quickly update expected values
157 | 4. `npm run lint` to make sure all the files and nice and linted
158 |
159 | ## Help Wanted
160 |
161 | I am always looking for sample data. If you have some results which you think are "interesting" then please raise an issue or pull request and we can add this to our sample data.
162 |
163 | ## Issues
164 |
165 | Raise any issues using GitHub and provide sample data where possible.
166 |
167 | To help debug any issues please provide the following info
168 |
169 | * node and npm version, refer to [Node](https://nodejs.org/en/) for LTS
170 | * xunit viewer version
171 | * browser
172 | * sample xml
173 |
174 |
175 | TODO
176 |
177 | 1. Fix CLI filtering
178 | 2. Release v11
179 | 3. Refactorings
180 | * Split components into files
181 | * Split reducer into files
182 | * Test all the things
--------------------------------------------------------------------------------
/src/cli/parse.js:
--------------------------------------------------------------------------------
1 | import xml2js from 'xml2js'
2 |
3 | const statusRank = [
4 | 'failure',
5 | 'error',
6 | 'passed',
7 | 'skipped',
8 | 'unknown'
9 | ]
10 |
11 | const parseString = (xml) => new Promise((resolve, reject) => {
12 | xml2js.parseString(xml, (err, result) => {
13 | if (err) reject(new Error(err))
14 | else resolve(result)
15 | })
16 | })
17 |
18 | const hashCode = (str) => {
19 | let hash = 0
20 | if (str.length === 0) return hash
21 | for (let i = 0; i < str.length; i++) {
22 | const char = str.charCodeAt(i)
23 | hash = ((hash << 5) - hash) + char
24 | hash = hash & hash
25 | }
26 | return hash
27 | }
28 |
29 | const extarctSuiteMeta = (output, testsuite) => {
30 | const meta = testsuite.$ || {}
31 | const name = meta.name || 'No Name'
32 | const id = hashCode(name)
33 | const suite = output.suites[id] || {}
34 | suite.tests = suite.tests || {}
35 | suite.systemOut = suite.systemOut || []
36 | suite.properties = suite.properties || {
37 | _visible: true
38 | }
39 |
40 | Object.entries(meta).forEach(([key, value]) => {
41 | if (!['errors', 'failures', 'name', 'skipped', 'tests', 'time'].includes(key)) {
42 | suite.properties[key] = suite.properties[key] || []
43 | suite.properties[key].push(value)
44 | }
45 | })
46 |
47 | suite.id = id
48 | suite.name = name
49 | suite.time = meta.time || 0
50 | return suite
51 | }
52 |
53 | const extractProperties = (suite, properties) => {
54 | suite.properties = suite.properties || {}
55 | suite.properties._visible = true
56 | properties.forEach(property => {
57 | if (typeof property === 'string') {
58 | property = property.trim()
59 | if (property !== '') {
60 | suite.properties['No Name'] = suite.properties['No Name'] || []
61 | suite.properties['No Name'].push(property)
62 | }
63 | } else {
64 | property.property.forEach(property => {
65 | const meta = property.$ || {}
66 | const name = meta.name || 'No Name'
67 | let value = meta.value || property._
68 | if (typeof property === 'string') value = property
69 | value = value || ''
70 | value = value.trim()
71 | suite.properties[name] = suite.properties[name] || []
72 | if (value) {
73 | suite.properties[name].push(value)
74 | }
75 | })
76 | }
77 | })
78 | }
79 |
80 | const extractTestMessages = (test, messages) => {
81 | messages.forEach(body => {
82 | const is_ = typeof body._ === 'string'
83 | const is$Message = typeof body.$ !== 'undefined' && ('message' in body.$)
84 | const is$Type = typeof body.$ !== 'undefined' && ('type' in body.$)
85 | const isString = typeof body === 'string'
86 |
87 | if (is_) test.messages.push(body._.trim())
88 | if (is$Message) test.messages.push(body.$.message.trim())
89 | if (is$Type) test.messages.push(body.$.type.trim())
90 | if (isString) test.messages.push(body.trim())
91 | })
92 | }
93 |
94 | const extractTests = (output, suite, testcases, defaultVisibility) => {
95 | suite.tests = suite.tests || {}
96 | testcases.forEach(testcase => {
97 | const meta = testcase.$ || {}
98 | const name = meta.name || 'No Name'
99 | const classname = meta.classname || meta.class || ''
100 | const time = meta.time || 0
101 | const id = hashCode(name + classname)
102 |
103 | const test = suite.tests[id] || { id, name, messages: [], visible: true }
104 | test.time = time
105 | test.classname = classname
106 | if (typeof testcase === 'string') test.messages.push(testcase.trim())
107 | if (testcase._) test.messages.push(testcase._.trim())
108 | if (meta.message) test.messages.push(testcase.$.message.trim())
109 | if (typeof testcase.properties !== 'undefined') {
110 | extractProperties(test, testcase.properties)
111 | delete testcase.properties
112 | }
113 | const clonedMeta = Object.assign({}, meta)
114 | delete clonedMeta.time
115 | delete clonedMeta.name
116 | delete clonedMeta.classname
117 | delete clonedMeta.class
118 | delete clonedMeta.message
119 | if (Object.keys(clonedMeta).length > 0) {
120 | const property = []
121 | for (const [name, value] of Object.entries(clonedMeta)) {
122 | property.push({
123 | $: {
124 | name, value
125 | }
126 | })
127 | }
128 | extractProperties(test, [{ property }])
129 | }
130 |
131 | if (typeof testcase !== 'string') {
132 | const keys = Object.keys(testcase).filter(key => key !== '$' && key !== '_' && key !== 'testcase')
133 | .sort((left, right) => {
134 | let leftStatus = statusRank.indexOf(left)
135 | let rightStatus = statusRank.indexOf(right)
136 | leftStatus = leftStatus === -1 ? statusRank.length : leftStatus
137 | rightStatus = rightStatus === -1 ? statusRank.length : rightStatus
138 |
139 | if (leftStatus < rightStatus) return -1
140 | if (leftStatus > rightStatus) return 1
141 | return 0
142 | })
143 | let status = keys[0]
144 | keys.forEach((key) => {
145 | if (key) extractTestMessages(test, testcase[key])
146 | })
147 | if (status === 'system-out') status = 'passed'
148 | test.status = status || 'passed'
149 | }
150 |
151 | test.messages = test.messages.filter(message => message !== '')
152 |
153 | test.visible = defaultVisibility[test.status]
154 | suite.tests[id] = test
155 | if (typeof testcase.testcase !== 'undefined') extractTests(output, suite, testcase.testcase, defaultVisibility)
156 | if (typeof testcase.testsuite !== 'undefined') extractSuite(output, testcase.testsuite, defaultVisibility)
157 | })
158 | }
159 |
160 | const extractSystemOut = (suite, testsuite) => {
161 | suite.systemOut = suite.systemOut || []
162 | let systemOut = testsuite['system-out']
163 | if (!Array.isArray(systemOut)) systemOut = [systemOut]
164 | suite.systemOut = suite.systemOut.concat(systemOut)
165 | }
166 |
167 | const extractSuite = (output, testsuites, defaultVisibility) => {
168 | if (!Array.isArray(testsuites)) testsuites = [testsuites]
169 | testsuites.forEach(testsuite => {
170 | const suite = extarctSuiteMeta(output, testsuite)
171 | if (typeof testsuite.properties !== 'undefined') extractProperties(suite, testsuite.properties)
172 | if (typeof testsuite.testcase !== 'undefined') extractTests(output, suite, testsuite.testcase, defaultVisibility)
173 | if (typeof testsuite['system-out'] !== 'undefined') extractSystemOut(suite, testsuite)
174 | output.suites[suite.id] = suite
175 | })
176 | }
177 |
178 | const extract = (output, testsuites, defaultVisibility) => {
179 | if (!Array.isArray(testsuites)) testsuites = [testsuites]
180 | testsuites.forEach(testsuite => {
181 | extractSuite(output, testsuite, defaultVisibility)
182 | if (typeof testsuite.testsuite !== 'undefined') extract(output, testsuite.testsuite, defaultVisibility)
183 | })
184 | }
185 |
186 | const parse = async (xml, defaultVisibility = { passed: true, failure: true, skipped: true, unknown: true, error: true }) => {
187 | const output = {
188 | suites: {}
189 | }
190 | const result = await parseString(xml)
191 | if (result.testsuites) {
192 | const testsuites = result.testsuites.testsuite
193 | extract(output, testsuites, defaultVisibility)
194 | } else if (result.testsuite) {
195 | extract(output, result.testsuite, defaultVisibility)
196 | }
197 |
198 | for (const value of Object.values(output.suites)) {
199 | value._visible = Object.keys(value.tests).length > 0 || Object.keys(value.properties).filter(prop => prop !== '_visible').length > 0
200 | value.systemOut = value.systemOut.map(value => value.trim())
201 | }
202 | return output
203 | }
204 |
205 | if (typeof window !== 'undefined') window.parse = parse
206 |
207 | export default parse
208 |
--------------------------------------------------------------------------------
/src/app/hero.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Logo from './logo.js'
3 | import useVisibility from './visible.js'
4 |
5 | const FilterIcon = () =>
6 | const GearIcon = () =>
7 | const BoltLightningIcon = () =>
8 | const PrintIcon = () =>
9 | const HeartIcon = () =>
10 | const HeartCheckIcon = () =>
11 | const HeartExclamationIcon = () =>
12 | const PassedIcon = () =>
13 | const FailureIcon = () =>
14 | const ErrorIcon = () =>
15 | const SkippedIcon = () =>
16 | const UnknownIcon = () =>
17 | const CloseIcon = () =>
18 |
19 | const FastFilter = ({ dropdown, dispatch }) => {
20 | const { fastFilter, setSearchParams } = useVisibility()
21 | return (
22 |
23 |
24 |
38 |
39 |
40 |
41 |
{
44 | setSearchParams({
45 | passed: false,
46 | skipped: false,
47 | error: true,
48 | failure: true,
49 | unknown: true
50 | })
51 | }}
52 | >
53 | ERROR/FAILURE/UNKNOWN
54 |
55 |
{
58 | setSearchParams({
59 | passed: true,
60 | skipped: true,
61 | error: false,
62 | failure: false,
63 | unknown: false
64 | })
65 | }}
66 | >
67 | PASSED/SKIPPED
68 |
69 |
{
72 | setSearchParams({
73 | passed: true,
74 | skipped: true,
75 | error: true,
76 | failure: true,
77 | unknown: true
78 | })
79 | }}
80 | >
81 | ALL
82 |
83 |
{ }}
86 | >
87 | FILTERED
88 |
89 |
90 |
91 |
92 | )
93 | }
94 |
95 | const Hero = ({
96 | active,
97 | onFilterClick,
98 | title,
99 | brand,
100 | printMode,
101 | burger,
102 | dropdown,
103 | dispatch,
104 | suites
105 | }) => {
106 | const totalSuites = Object.values(suites).length
107 | const visibleSuiteCount = Object.values(suites).filter(({ _visible }) => _visible).length
108 |
109 | let propertiesTotal = 0
110 | let visiblePropertyCount = 0
111 | Object.entries(suites).forEach(([key, suite]) => {
112 | const found = Object.keys(suite.properties).filter(key => key !== '_active' && key !== '_visible').length
113 | propertiesTotal += found
114 | if (suite._visible) visiblePropertyCount += found
115 | Object.values(suite.tests).forEach(test => {
116 | if (test.properties) {
117 | const found = Object.keys(test.properties).filter(key => key !== '_active' && key !== '_visible').length
118 | propertiesTotal += found
119 | if (test.visible) visiblePropertyCount += found
120 | }
121 | })
122 | })
123 |
124 | const testCounts = {
125 | total: { passed: 0, error: 0, failure: 0, skipped: 0, unknown: 0 },
126 | visible: { passed: 0, error: 0, failure: 0, skipped: 0, unknown: 0 }
127 | }
128 | Object.values(suites).forEach(({ tests }) => {
129 | Object.values(tests).forEach(test => {
130 | testCounts.total[test.status] += 1
131 | if (test.visible) {
132 | testCounts.visible[test.status] += 1
133 | }
134 | })
135 | })
136 |
137 | return
138 |
139 |
195 |
196 |
197 | }
198 |
199 | export default Hero
200 |
--------------------------------------------------------------------------------
/src/cli/parse-expected.json:
--------------------------------------------------------------------------------
1 | {
2 | "suites": {
3 | "390294658": {
4 | "tests": {
5 | "-1319400288": {
6 | "id": -1319400288,
7 | "name": "testsuite in a testcase testcase",
8 | "messages": [],
9 | "visible": true,
10 | "time": 0,
11 | "classname": "",
12 | "status": "passed"
13 | }
14 | },
15 | "systemOut": [],
16 | "properties": {
17 | "_visible": true
18 | },
19 | "id": 390294658,
20 | "name": "testsuite in a testcase",
21 | "time": 0,
22 | "_visible": true
23 | },
24 | "1201687819": {
25 | "tests": {
26 | "-2126675373": {
27 | "id": -2126675373,
28 | "name": "testcase 1",
29 | "messages": [],
30 | "visible": true,
31 | "time": 0,
32 | "classname": "",
33 | "status": "passed"
34 | },
35 | "-2126675372": {
36 | "id": -2126675372,
37 | "name": "testcase 2",
38 | "messages": [],
39 | "visible": true,
40 | "time": 0,
41 | "classname": "",
42 | "status": "passed"
43 | }
44 | },
45 | "systemOut": [],
46 | "properties": {
47 | "_visible": true
48 | },
49 | "id": 1201687819,
50 | "name": "duplicate",
51 | "time": 0,
52 | "_visible": true
53 | },
54 | "1870281583": {
55 | "tests": {
56 | "31726797": {
57 | "id": 31726797,
58 | "name": "with properties",
59 | "messages": [
60 | "message 1"
61 | ],
62 | "visible": true,
63 | "time": 0,
64 | "classname": "",
65 | "properties": {
66 | "_visible": true,
67 | "name only": [],
68 | "prop 1": [
69 | "value 1",
70 | "value 2"
71 | ],
72 | "prop 2": [
73 | "value"
74 | ],
75 | "No Name": [
76 | "value with no name",
77 | "value only"
78 | ],
79 | "seperate props": [
80 | "value"
81 | ]
82 | },
83 | "status": "passed"
84 | }
85 | },
86 | "systemOut": [],
87 | "properties": {
88 | "_visible": true
89 | },
90 | "id": 1870281583,
91 | "name": "testcase with properties",
92 | "time": 0,
93 | "_visible": true
94 | },
95 | "-1861124855": {
96 | "tests": {
97 | "152530127": {
98 | "id": 152530127,
99 | "name": "simple passing test",
100 | "messages": [],
101 | "visible": true,
102 | "time": 0,
103 | "classname": "",
104 | "status": "passed"
105 | },
106 | "448559065": {
107 | "id": 448559065,
108 | "name": "test with time",
109 | "messages": [],
110 | "visible": true,
111 | "time": "time",
112 | "classname": "",
113 | "status": "passed"
114 | },
115 | "1350225396": {
116 | "id": 1350225396,
117 | "name": "failing test with messages",
118 | "messages": [
119 | "inner massage 1",
120 | "message prop 1",
121 | "inner massage 2",
122 | "message prop 2"
123 | ],
124 | "visible": true,
125 | "time": 0,
126 | "classname": "",
127 | "status": "failure"
128 | },
129 | "1443479640": {
130 | "id": 1443479640,
131 | "name": "test with messages",
132 | "messages": [
133 | "inner message",
134 | "message prop"
135 | ],
136 | "visible": true,
137 | "time": 0,
138 | "classname": "",
139 | "status": "passed"
140 | },
141 | "1669557569": {
142 | "id": 1669557569,
143 | "name": "the sub test",
144 | "messages": [],
145 | "visible": true,
146 | "time": 0,
147 | "classname": "",
148 | "status": "passed"
149 | },
150 | "1879989598": {
151 | "id": 1879989598,
152 | "name": "erroring test with messages",
153 | "messages": [
154 | "inner massage 1",
155 | "message prop 1",
156 | "inner massage 2",
157 | "message prop 2"
158 | ],
159 | "visible": true,
160 | "time": 0,
161 | "classname": "",
162 | "status": "error"
163 | },
164 | "2045602436": {
165 | "id": 2045602436,
166 | "name": "test passed with messages",
167 | "messages": [
168 | "inner massage 1",
169 | "message prop 1",
170 | "inner massage 2",
171 | "message prop 2"
172 | ],
173 | "visible": true,
174 | "time": 0,
175 | "classname": "",
176 | "status": "passed"
177 | },
178 | "-579348086": {
179 | "id": -579348086,
180 | "name": "No Name",
181 | "messages": [
182 | "only a message"
183 | ],
184 | "time": 0,
185 | "classname": ""
186 | },
187 | "-232186498": {
188 | "id": -232186498,
189 | "name": "test with sub test",
190 | "messages": [],
191 | "visible": true,
192 | "time": 0,
193 | "classname": "",
194 | "status": "passed"
195 | },
196 | "-986143044": {
197 | "id": -986143044,
198 | "name": "test passed with messages and free text",
199 | "messages": [
200 | "HERE IS SOME TEXT",
201 | "inner massage 1",
202 | "message prop 1"
203 | ],
204 | "visible": true,
205 | "time": 0,
206 | "classname": "",
207 | "status": "passed"
208 | },
209 | "-1154650731": {
210 | "id": -1154650731,
211 | "name": "skiping test with messages",
212 | "messages": [
213 | "inner massage 1",
214 | "message prop 1",
215 | "inner massage 2",
216 | "message prop 2"
217 | ],
218 | "visible": true,
219 | "time": 0,
220 | "classname": "",
221 | "status": "skipped"
222 | },
223 | "-198845521": {
224 | "id": -198845521,
225 | "name": "error and failure test with messages",
226 | "messages": [
227 | "failure inner massage 2",
228 | "failure message prop 2",
229 | "error inner massage 1",
230 | "error message prop 1"
231 | ],
232 | "visible": true,
233 | "time": 0,
234 | "classname": "",
235 | "status": "failure"
236 | }
237 | },
238 | "systemOut": [],
239 | "properties": {
240 | "_visible": true,
241 | "name only": [],
242 | "prop 1": [
243 | "value 1",
244 | "value 2"
245 | ],
246 | "prop 2": [
247 | "value"
248 | ],
249 | "No Name": [
250 | "value with no name",
251 | "value only"
252 | ],
253 | "seperate props": [
254 | "value"
255 | ]
256 | },
257 | "id": -1861124855,
258 | "name": "suite 1",
259 | "time": "time",
260 | "_visible": true
261 | },
262 | "-1861124854": {
263 | "tests": {
264 | "-1146345790": {
265 | "id": -1146345790,
266 | "name": "testcase",
267 | "messages": [],
268 | "visible": true,
269 | "time": 0,
270 | "classname": "",
271 | "status": "passed"
272 | }
273 | },
274 | "systemOut": [],
275 | "properties": {
276 | "_visible": true
277 | },
278 | "id": -1861124854,
279 | "name": "suite 2",
280 | "time": 0,
281 | "_visible": true
282 | },
283 | "-1028948070": {
284 | "tests": {
285 | "-1387098835": {
286 | "id": -1387098835,
287 | "name": "testcase duplicate",
288 | "messages": [
289 | "message 1",
290 | "message 2"
291 | ],
292 | "visible": true,
293 | "time": 0,
294 | "classname": "",
295 | "status": "passed"
296 | }
297 | },
298 | "systemOut": [],
299 | "properties": {
300 | "_visible": true
301 | },
302 | "id": -1028948070,
303 | "name": "suite with duplicate tests",
304 | "time": 0,
305 | "_visible": true
306 | }
307 | }
308 | }
--------------------------------------------------------------------------------
/src/app/suite.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import X from 'react-render-if-visible'
3 | import Toggle from './toggle.js'
4 | import SuiteCount from './suite-count.js'
5 | import Y from 'react-linkify'
6 | import linkify from 'linkify-html'
7 | import useVisibility from './visible.js'
8 | const Linkify = Y.default
9 |
10 | const RenderIfVisible = X.default
11 | const RenderAlways = (props) =>
12 |
13 | const icons = {
14 | passed: 'check',
15 | failure: 'times',
16 | error: 'exclamation',
17 | skipped: 'ban',
18 | unknown: 'question'
19 | }
20 |
21 | const statusRank = [
22 | 'failure',
23 | 'error',
24 | 'passed',
25 | 'skipped',
26 | 'unknown'
27 | ]
28 |
29 | const Properties = ({ properties, active = true, dispatch, suite, test = null }) => {
30 | return
31 |
39 | {active
40 | ?
41 |
42 |
43 |
44 | | Property |
45 | Value |
46 |
47 |
48 |
49 | {Object.keys(properties)
50 | .filter(key => key !== '_active' && key !== '_visible')
51 | .map(key => {
52 | return
53 | | {key} |
54 | {properties[key].join(', ')} |
55 |
56 | })}
57 |
58 |
59 |
60 | : null}
61 |
62 | }
63 |
64 | const RawContent = ({ messages }) =>
65 | {messages.map((message, index) =>
{message})}
66 |
67 |
68 | const PrettyContent = ({ messages }) =>
69 | {messages.map((message, index) =>
)}
70 |
71 |
72 | const PrettyIcon = () =>
73 |
74 |
75 |
76 | const CodeIcon = () =>
77 |
78 |
79 |
80 | const Test = ({
81 | printMode,
82 | id,
83 | messages,
84 | status,
85 | time,
86 | classname,
87 | name,
88 | properties = {},
89 | active = true,
90 | raw = true,
91 | dispatch,
92 | suite
93 | }) => {
94 | const hasProperties = properties._visible & Object.keys(properties).filter(key => key !== '_active' && key !== '_visible').length > 0
95 | const hasMessage = messages.length > 0
96 | const Wrapper = printMode ? RenderAlways : RenderIfVisible
97 |
98 | return (
99 |
100 |
101 |
118 |
119 | {active && (hasMessage || hasProperties)
120 | ?
121 | {hasProperties ?
: null}
122 | {
123 | hasMessage
124 | ? <>
125 |
}
129 | offIcon={
}
130 | offLabel='pretty'
131 | onChange={() => dispatch({ type: 'toggle-test-mode', payload: { suite, id, raw: !raw } })} />
132 | {
133 | raw
134 | ?
135 | :
136 | }
137 | >
138 | : null
139 | }
140 |
141 | : null}
142 |
143 |
144 |
145 | )
146 | }
147 |
148 | const Suite = ({
149 | visible,
150 | id,
151 | name,
152 | active = false,
153 | properties = {},
154 | time,
155 | tests = {},
156 | dispatch,
157 | systemOut = [],
158 | printMode = false
159 | }) => {
160 | let passed = 0
161 | let failure = 0
162 | let skipped = 0
163 | let error = 0
164 | let unknown = 0
165 | Object.keys(tests).forEach(key => {
166 | const status = tests[key].status
167 | if (status === 'passed') passed += 1
168 | else if (status === 'failure') failure += 1
169 | else if (status === 'skipped') skipped += 1
170 | else if (status === 'error') error += 1
171 | else unknown += 1
172 | })
173 |
174 | const Wrapper = printMode ? RenderAlways : RenderIfVisible
175 |
176 | const { query } = useVisibility()
177 |
178 | const hasTests = Object.keys(tests).length > 0 && Object.values(tests).some(test => query[test.status])
179 | const hasProperties = '_visible' in properties && properties._visible && Object.keys(properties).filter(key => key !== '_active' && key !== '_visible').length > 0
180 | const containsSomething = hasTests || hasProperties
181 | return (
182 |
183 |
184 |
207 | {active && containsSomething
208 | ?
209 |
210 | {systemOut.length > 0 ? systemOut.map((value, index) =>
{value}) : null}
211 | {hasProperties ?
: null}
212 |
213 | {
214 | Object.entries(tests)
215 | .filter(([key, test]) => query[test.status])
216 | .sort((left, right) => {
217 | let leftStatus = statusRank.indexOf(left[1].status)
218 | let rightStatus = statusRank.indexOf(right[1].status)
219 |
220 | leftStatus = leftStatus === -1 ? statusRank.length : leftStatus
221 | rightStatus = rightStatus === -1 ? statusRank.length : rightStatus
222 |
223 | if (leftStatus < rightStatus) {
224 | return -2
225 | }
226 | if (leftStatus > rightStatus) return 2
227 |
228 | const leftName = left[1].name
229 | const rightName = right[1].name
230 |
231 | if (leftName < rightName) return -1
232 | if (leftName > rightName) return 1
233 |
234 | return 0
235 | })
236 | .map(([key, test]) => )
237 | }
238 |
239 |
240 |
241 | : null}
242 |
243 |
244 | )
245 | }
246 |
247 | export default Suite
248 |
--------------------------------------------------------------------------------
/src/app/reducer.js:
--------------------------------------------------------------------------------
1 | import fuzzy from 'fuzzy'
2 | import merge from 'merge'
3 |
4 | const toggleAllProperties = (state, payload, update, toggleType, suiteTesttoggleType) => {
5 | update[toggleType] = state[toggleType]
6 | update[toggleType][payload.type] = payload.active
7 |
8 | if (payload.type === 'all') {
9 | update[toggleType].suites = payload.active
10 | update[toggleType].tests = payload.active
11 | }
12 |
13 | if (payload.type === 'all' || payload.type === 'suites') {
14 | Object.values(update.currentSuites).forEach(suite => {
15 | suite.properties[suiteTesttoggleType] = payload.active
16 | })
17 | }
18 |
19 | if (payload.type === 'all' || payload.type === 'tests') {
20 | Object.values(update.currentSuites).forEach(suite => {
21 | Object.values(suite.tests).forEach(test => {
22 | if ('properties' in test) {
23 | test.properties[suiteTesttoggleType] = payload.active
24 | }
25 | })
26 | })
27 | }
28 | return update
29 | }
30 |
31 | export default (state, { type, payload }) => {
32 | let update = {}
33 | update.currentSuites = state.currentSuites
34 |
35 | if (type === 'parse-error') {
36 | state = merge.recursive(true, {}, state)
37 | state.errors = state.errors || []
38 | state.errors.push({
39 | error: payload.error,
40 | file: payload.file
41 | })
42 | }
43 |
44 | if (type === 'parse-suites') {
45 | state = merge.recursive(true, {}, state)
46 | state.suites = payload.suites
47 | state.currentSuites = payload.suites
48 | Object.values(state.currentSuites).forEach(suite => {
49 | if (Object.keys(suite.tests).length > 0 || Object.keys(suite.properties).length > 0) suite.active = true
50 | })
51 | }
52 |
53 | if (type === 'search-suites') {
54 | Object.values(state.suites).forEach(({ name, id }) => {
55 | if (fuzzy.test(payload.value.toLowerCase(), name.toLowerCase())) {
56 | update.currentSuites[id] = update.currentSuites[id] || merge.recursive(true, {}, state.suites[id])
57 | if (!('active' in update.currentSuites[id])) update.currentSuites[id].active = true
58 | } else delete update.currentSuites[id]
59 | })
60 | update.suitesExpanded = Object.values(update.currentSuites).some(suite => suite.active === true)
61 | }
62 | if (type === 'search-tests') {
63 | Object.values(state.suites).forEach(suite => {
64 | Object.values(suite.tests).forEach(test => {
65 | if (!fuzzy.test(payload.value.toLowerCase(), test.name.toLowerCase()) && !test.messages.some(message => fuzzy.test(payload.value.toLowerCase(), message.toLowerCase()))) {
66 | if (update.currentSuites[suite.id]) delete update.currentSuites[suite.id].tests[test.id]
67 | } else if (suite.id in update.currentSuites && !(test.id in update.currentSuites[suite.id].tests)) {
68 | if (update.currentSuites[suite.id]) {
69 | update.currentSuites[suite.id].tests[test.id] = merge.recursive(true, {}, state.suites[suite.id].tests[test.id])
70 | update.currentSuites[suite.id].tests[test.id].active = true
71 | update.currentSuites[suite.id].tests[test.id].visible = true
72 | update.currentSuites[suite.id].tests[test.id].raw = true
73 | }
74 | }
75 | })
76 | })
77 | }
78 | if (type === 'search-properties') {
79 | Object.values(state.suites).forEach(suite => {
80 | Object.entries(suite.properties)
81 | .filter(([key]) => key !== '_visible' && key !== '_active')
82 | .forEach(([key, values]) => {
83 | values = values || []
84 | if (!fuzzy.test(payload.value.toLowerCase(), key.toLowerCase()) && !values.some(value => fuzzy.test(payload.value.toLowerCase(), value.toLowerCase()))) delete update.currentSuites[suite.id].properties[key]
85 | else if (suite.id in update.currentSuites && !(key in update.currentSuites[suite.id].properties)) {
86 | if (update.currentSuites[suite.id]) {
87 | update.currentSuites[suite.id].properties[key] = [].concat(state.suites[suite.id].properties[key])
88 | update.currentSuites[suite.id].properties._active = true
89 | update.currentSuites[suite.id].properties._visible = true
90 | update.propertiesExpanded = false
91 | }
92 | }
93 | })
94 | })
95 | update.propertiesExpanded = Object.values(update.currentSuites).some((suite) => {
96 | return suite.properties._active || false
97 | })
98 | update.propertiesVisible = Object.values(update.currentSuites).some((suite) => {
99 | return suite.properties._visible || false
100 | })
101 | }
102 |
103 | if (type === 'toggle-all-suites') {
104 | update.suitesExpanded = !state.suitesExpanded
105 | Object.values(update.currentSuites).forEach(suite => { suite.active = update.suitesExpanded })
106 | }
107 | if (type === 'toggle-empty-suites') {
108 | update.suitesEmpty = !state.suitesEmpty
109 | }
110 | if (type === 'toggle-menu') update.menuActive = !state.menuActive
111 | if (type === 'toggle-suite-options') update.suiteOptionsActive = !state.suiteOptionsActive
112 | if (type === 'toggle-test-options') update.testOptionsActive = !state.testOptionsActive
113 | if (type === 'toggle-properties-options') update.propertiesOptionsActive = !state.propertiesOptionsActive
114 | if (type === 'toggle-files') update.activeFiles = !state.activeFiles
115 | if (type === 'toggle-suite') {
116 | update.currentSuites[payload.id].active = payload.active
117 | update.suitesExpanded = Object.values(update.currentSuites).some(suite => suite.active === true)
118 | }
119 |
120 | if (type === 'toggle-properties') {
121 | if (typeof payload.test !== 'undefined' && payload.test !== null) {
122 | update.currentSuites[payload.suite].tests[payload.test].properties._active = payload.active
123 | } else {
124 | update.currentSuites[payload.suite].properties._active = payload.active
125 | update.propertiesExpanded = Object.values(update.currentSuites).some((suite) => {
126 | return suite.properties._active || false
127 | })
128 | }
129 | }
130 |
131 | if (type === 'toggle-all-properties') {
132 | update = toggleAllProperties(state, payload, update, 'propertiesExpanded', '_active')
133 | }
134 | if (type === 'toggle-properties-visbility') {
135 | update = toggleAllProperties(state, payload, update, 'propertiesVisible', '_visible')
136 | }
137 |
138 | if (type === 'toggle-test') {
139 | update.currentSuites[payload.suite].tests[payload.id].active = payload.active
140 | }
141 | if (type === 'toggle-test-mode') {
142 | update.currentSuites[payload.suite].tests[payload.id].raw = payload.raw
143 | }
144 |
145 | if (type === 'toggle-test-expanded') {
146 | update.testToggles = state.testToggles
147 | update.testToggles[payload.status].expanded = payload.active
148 |
149 | Object.values(update.currentSuites).forEach(suite => {
150 | Object.values(suite.tests).forEach(test => {
151 | if (payload.status === 'all') test.active = payload.active
152 | else if (payload.status === test.status) test.active = payload.active
153 | else if (typeof test.status === 'undefined' && payload.status === 'unknown') test.active = payload.active
154 | })
155 | })
156 |
157 | if (payload.status === 'all') {
158 | update.testToggles.passed.expanded = payload.active
159 | update.testToggles.failure.expanded = payload.active
160 | update.testToggles.error.expanded = payload.active
161 | update.testToggles.skipped.expanded = payload.active
162 | update.testToggles.unknown.expanded = payload.active
163 | } else {
164 | if (update.testToggles.passed.expanded &&
165 | update.testToggles.failure.expanded &&
166 | update.testToggles.error.expanded &&
167 | update.testToggles.skipped.expanded &&
168 | update.testToggles.unknown.expanded) update.testToggles.all.expanded = true
169 | else update.testToggles.all.expanded = false
170 | }
171 | }
172 |
173 | if (type === 'toggle-test-raw') {
174 | update.testToggles = state.testToggles
175 | update.testToggles[payload.status].raw = payload.active
176 |
177 | Object.values(update.currentSuites).forEach(suite => {
178 | Object.values(suite.tests).forEach(test => {
179 | if (payload.status === 'all') test.raw = payload.active
180 | else if (payload.status === test.status) test.raw = payload.active
181 | else if (typeof test.status === 'undefined' && payload.status === 'unknown') test.raw = payload.active
182 | })
183 | })
184 |
185 | if (payload.status === 'all') {
186 | update.testToggles.passed.raw = payload.active
187 | update.testToggles.failure.raw = payload.active
188 | update.testToggles.error.raw = payload.active
189 | update.testToggles.skipped.raw = payload.active
190 | update.testToggles.unknown.raw = payload.active
191 | } else {
192 | if (update.testToggles.passed.raw &&
193 | update.testToggles.failure.raw &&
194 | update.testToggles.error.raw &&
195 | update.testToggles.skipped.raw &&
196 | update.testToggles.unknown.raw) update.testToggles.all.raw = true
197 | }
198 | }
199 |
200 | if (type === 'hero-burger') {
201 | const { burger } = state.hero
202 | update.hero = update.hero || {}
203 | update.hero.burger = !burger
204 | }
205 |
206 | if (type === 'hero-dropdown') {
207 | const { dropdown } = state.hero
208 | update.hero = update.hero || {}
209 | update.hero.dropdown = !dropdown
210 | }
211 |
212 | if (type === 'hero-print-mode') {
213 | const { printMode } = state
214 | update.printMode = !printMode
215 | }
216 |
217 | if (type === 'print-mode') {
218 | update.printMode = payload.printMode
219 | }
220 |
221 | state = merge.recursive(true, state, update)
222 |
223 | state = merge.recursive(true, state, update)
224 |
225 | Object.values(state.currentSuites).forEach(suite => {
226 | if (!state.suitesEmpty) suite._visible = true
227 | else suite._visible = (Object.keys(suite.tests).length > 0 && Object.values(suite.tests).filter(test => test.visible).length > 0) || (suite.properties._visible && Object.keys(suite.properties).filter(prop => prop !== '_visible').length > 0)
228 | })
229 |
230 | return state
231 | }
232 |
--------------------------------------------------------------------------------
/src/app/index.css:
--------------------------------------------------------------------------------
1 | body,
2 | .card {
3 | color: #2C3E50 !important;
4 | }
5 |
6 | .filter {
7 | transition: background-color 0.2s ease-in-out;
8 | background-color: #2C3E50 !important;
9 | border: none;
10 | }
11 |
12 | .filter:hover,
13 | .filter:active,
14 | .filter:focus {
15 | background-color: rgb(52, 78, 104) !important;
16 | }
17 |
18 | .card-header-icon.is-hidden-tablet {
19 | max-width: 48px;
20 | }
21 |
22 | .navbar-brand {
23 | align-items: center;
24 | }
25 |
26 | .navbar-burger {
27 | color: white;
28 | }
29 |
30 | .navbar-menu {
31 | background-color: transparent !important;
32 | }
33 |
34 | .navbar-end {
35 | align-items: center;
36 | }
37 |
38 | .navbar-item.dropdown {
39 | padding-left: 0 !important;
40 | }
41 |
42 | .dropdown-item.is-disabled {
43 | filter: grayscale(1);
44 | opacity: 0.5;
45 | pointer-events: none;
46 | }
47 |
48 | @media screen and (min-width: 1024px) {
49 | .dropdown-trigger {
50 | min-width: 280px;
51 | }
52 |
53 | .dropdown-trigger .filter {
54 | float: right;
55 | }
56 | }
57 |
58 | .navbar-item .table {
59 | background-color: transparent !important;
60 | color: white !important;
61 | }
62 |
63 | .hero {
64 | background-color: #2C3E50 !important;
65 | padding: 10px !important;
66 | }
67 |
68 | .hero .container {
69 | width: 100%;
70 | }
71 |
72 | .Burger {
73 | height: 100%;
74 | }
75 |
76 | .hero-center {
77 | display: flex;
78 | justify-content: center;
79 | align-items: center;
80 | }
81 |
82 | .hero .brand,
83 | .hero .logo {
84 | height: 64px;
85 | width: auto;
86 | margin-right: 16px;
87 | }
88 |
89 | .hero .title {
90 | font-size: 36px;
91 | font-weight: normal;
92 | }
93 |
94 | .card-content {
95 | overflow-x: auto;
96 | overflow-y: hidden;
97 | }
98 |
99 | .card-header-icon {
100 | flex-grow: 1;
101 | background-color: transparent;
102 | border: none;
103 | font-size: 16px;
104 | color: #2C3E50;
105 | }
106 |
107 | .card-header-icon .icon {
108 | margin-left: auto;
109 | transition: transform 0.2s ease-in-out;
110 | transform: rotate(180deg);
111 | }
112 |
113 | .options:first-child {
114 | margin-top: 0.1rem;
115 | }
116 |
117 | .options.is-active .card-header-icon .icon,
118 | .files.is-active .card-header-icon .icon {
119 | transform: rotate(0deg);
120 | }
121 |
122 | .options-inputs {
123 | display: flex;
124 | align-items: center;
125 | overflow-x: auto;
126 | overflow-y: hidden;
127 | }
128 |
129 | .options-search {
130 | margin-bottom: 0 !important;
131 | margin-right: 32px;
132 | margin-left: 16px;
133 | min-width: 100px;
134 | }
135 |
136 | .input {
137 | border: none;
138 | border-radius: 0;
139 | box-shadow: none;
140 | border-bottom: 1px solid #2C3E50;
141 | padding: 0;
142 | }
143 |
144 | .options-total,
145 | .options-count {
146 | margin-right: 24px;
147 | font-size: 16px;
148 | }
149 |
150 | .options-count {
151 | display: flex;
152 | }
153 |
154 | .options-count .icon {
155 | font-size: 12px;
156 | }
157 |
158 | .options-toggles .toggle {
159 | justify-content: left;
160 | margin-right: 1em;
161 | }
162 |
163 | .options,
164 | .files {
165 | box-shadow: none;
166 | border-bottom: solid 1px #E7EAED;
167 | }
168 |
169 | .options .card-header,
170 | .files .card-header {
171 | box-shadow: none;
172 | }
173 |
174 | .options .card-header {
175 | padding: 10px 0;
176 | }
177 |
178 | .options .card-header-icon {
179 | padding: 0;
180 | }
181 |
182 | .options .card-header-icon .icon:last-child {
183 | margin-left: auto !important;
184 | margin-right: 14px !important;
185 | }
186 |
187 | .options-total .icon {
188 | font-size: 12px;
189 | transform: rotate(0);
190 | margin-right: 4px !important;
191 | }
192 |
193 | .options .card-content {
194 | transition: height 0.2s ease-in-out, padding 0.2s ease-in-out;
195 | height: 0;
196 | padding: 0 1.5rem;
197 | overflow: hidden;
198 | }
199 |
200 | .options.is-active .card-content {
201 | padding: 1.5rem;
202 | height: auto;
203 | }
204 |
205 | .properties-options-toggle {
206 | margin-right: 1rem;
207 | }
208 |
209 |
210 | .test-options-toggle-row {
211 | display: flex;
212 | }
213 |
214 | .properties-options-toggle-label,
215 | .test-options-toggle-row-label {
216 | color: #5e5e5e;
217 | display: inline-block;
218 | }
219 |
220 | .test-options-toggle-row-label {
221 | width: 100px;
222 | }
223 |
224 | .properties-options-toggle-label {
225 | width: 60px;
226 | }
227 |
228 | .test-options-toggle-row-label .icon {
229 | font-size: 14px;
230 | transform: translateY(1px);
231 | }
232 |
233 | .files .card-content {
234 | transition: height 0.2s ease-in-out, padding 0.2s ease-in-out;
235 | }
236 |
237 | .files .card-content {
238 | height: 0;
239 | padding: 0 1.5rem;
240 | }
241 |
242 | .files.is-active .card-content {
243 | height: 395px;
244 | }
245 |
246 | .toggles-container {
247 | display: flex;
248 | }
249 |
250 | .toggles {
251 | margin-right: 16px;
252 | width: 120px;
253 | }
254 |
255 | .toggles .title {
256 | margin-bottom: 4px;
257 | display: flex;
258 | align-items: center;
259 | }
260 |
261 | .toggles .title .icon {
262 | font-size: 14px;
263 | height: 100%;
264 | margin-left: -6px;
265 | }
266 |
267 | .toggle {
268 | border: none;
269 | margin-bottom: 4px;
270 | padding: 0;
271 | height: auto;
272 | }
273 |
274 | .toggle-rail {
275 | position: relative;
276 | margin-right: 2px;
277 | background-color: #2C3E50;
278 | width: 20px;
279 | height: 8px;
280 | border-radius: 4px;
281 | margin-right: 8px;
282 | }
283 |
284 | .toggle-handle {
285 | transition: left 0.1s ease-in-out;
286 | position: absolute;
287 | top: -3px;
288 | height: 14px;
289 | width: 14px;
290 | border-radius: 90px;
291 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.25);
292 | left: -1px;
293 | background-color: #F5F9FC;
294 | }
295 |
296 | .toggle.is-active .toggle-handle {
297 | background-color: #375C80;
298 | left: 8px;
299 | }
300 |
301 | .files .card-content {
302 | padding: 0;
303 | }
304 |
305 | .tabs {
306 | margin: 0 !important;
307 | margin-top: 1px !important;
308 | }
309 |
310 | .files li a {
311 | border-radius: 0 !important;
312 | }
313 |
314 | .files li a .delete {
315 | margin-left: 6px;
316 | }
317 |
318 | .files-input {
319 | margin-left: 16px;
320 | margin-bottom: 16px;
321 | width: 98%;
322 | }
323 |
324 | .add-file {
325 | padding: 0.5em 0 !important;
326 | }
327 |
328 | .files .subtitle {
329 | padding-left: 4px;
330 | margin-bottom: 0 !important;
331 | }
332 |
333 | .files .subtitle .icon {
334 | transform: rotate(0) translateY(2px) !important;
335 | font-size: 18px;
336 | margin-right: 4px;
337 | }
338 |
339 | .suite {
340 | transition: box-shadow 0.1s ease-in-out, margin 0.1s ease-in-out;
341 | background-color: #F7F7F7;
342 | margin: 12px 0;
343 | }
344 |
345 | .suite .card-header {
346 | transition: background-color 0.2s ease-in-out;
347 | border: none;
348 | width: 100%;
349 | display: flex;
350 | flex-wrap: wrap;
351 | }
352 |
353 | .suite-count-container {
354 | width: 100%;
355 | text-align: left;
356 | }
357 |
358 | .suite .card-header:hover {
359 | cursor: pointer;
360 | }
361 |
362 | .suite .card-header:hover,
363 | .suite .card-header:active,
364 | .suite .card-header:focus {
365 | background-color: #ebebeb;
366 | }
367 |
368 | .suite.is-empty .card-header {
369 | background-color: #F7F7F7;
370 | }
371 |
372 | .suite.is-empty .card-header:hover {
373 | cursor: initial;
374 | }
375 |
376 |
377 | .suite.is-active {
378 | margin: 24px 0;
379 | }
380 |
381 | .suite.is-inactive,
382 | .suite.is-empty {
383 | box-shadow: none;
384 | margin: 8px 0;
385 | }
386 |
387 | .suites {
388 | margin-top: 32px !important;
389 | }
390 |
391 | .suite .card {
392 | margin-bottom: 16px;
393 | }
394 |
395 | .suite .card:last-child {
396 | margin-bottom: 0;
397 | }
398 |
399 | .properties .card-header-icon svg,
400 | .suite .card-header-icon svg {
401 | transition: transform 0.2s ease-in-out;
402 | }
403 |
404 | .properties.is-active .card-header-icon svg,
405 | .suite.is-active .card-header-icon svg {
406 | transform: rotate(180deg);
407 | }
408 |
409 | .suite .card-header-title,
410 | .suite .card-header-icon {
411 | padding: 0;
412 | }
413 |
414 | .suite .card-header-title {
415 | margin-left: 16px;
416 | }
417 |
418 | .suite .card-header-icon {
419 | margin-right: 16px;
420 | }
421 |
422 | .suite .card-header {
423 | padding: 0.75rem;
424 | }
425 |
426 | .card-header-title small {
427 | font-weight: normal;
428 | margin-left: 8px;
429 | }
430 |
431 | .test .card-header-title,
432 | .properties .card-header-title {
433 | margin-bottom: 0 !important;
434 | }
435 |
436 | .properties .card-content {
437 | padding: 0;
438 | padding-top: 1px;
439 | }
440 |
441 | .test>.card-header>.card-header-title {
442 | color: #fff;
443 | }
444 |
445 | .test>.card-header>.card-header-icon {
446 | color: #fff;
447 | }
448 |
449 | .test.is-empty>.card-header,
450 | .test>.card-header {
451 | background-color: #4B85FA;
452 | }
453 |
454 | .test.is-populated>.card-header:hover,
455 | .test.is-populated>.card-header:active,
456 | .test.is-populated>.card-header:focus {
457 | background-color: #2f5bb4;
458 | }
459 |
460 | .test.is-passed>.card-header {
461 | background-color: #0DBF1F;
462 | }
463 |
464 | .test.is-populated.is-passed>.card-header:hover,
465 | .test.is-populated.is-passed>.card-header:active,
466 | .test.is-populated.is-passed>.card-header:focus {
467 | background-color: #25a132;
468 | }
469 |
470 | .test.is-failure>.card-header {
471 | background-color: #B32010;
472 | }
473 |
474 | .test.test.is-populated.is-failure>.card-header:hover,
475 | .test.test.is-populated.is-failure>.card-header:active,
476 | .test.test.is-populated.is-failure>.card-header:focus {
477 | background-color: #963126;
478 | }
479 |
480 | .test.is-error>.card-header {
481 | background-color: #E5CB05;
482 | }
483 |
484 |
485 | .test.is-populated.is-error>.card-header:hover,
486 | .test.is-populated.is-error>.card-header:active,
487 | .test.is-populated.is-error>.card-header:focus {
488 | background-color: #c7b319;
489 | }
490 |
491 | .test.is-skipped>.card-header {
492 | background-color: #adb2b6;
493 | }
494 |
495 | .test.is-populated.is-skipped>.card-header:hover,
496 | .test.is-populated.is-skipped>.card-header:active,
497 | .test.is-populated.is-skipped>.card-header:focus {
498 | background-color: #828588;
499 | }
500 |
501 | .test.is-active .fa-angle-down {
502 | transform: rotate(180deg);
503 | }
504 |
505 |
506 | .suite.is-empty,
507 | .suite.is-empty .card-header .test.is-empty,
508 | .test.is-empty .card-header {
509 | box-shadow: none;
510 | opacity: 0.7;
511 | cursor: initial;
512 | }
513 |
514 | .card-header {
515 | user-select: text;
516 | }
517 |
518 | .test.card {
519 | transition: margin 0.1s ease-in-out;
520 | margin: 8px 0;
521 | }
522 |
523 | .test.card.is-populated.is-active {
524 | margin: 24px 0;
525 | }
526 |
527 | .suite-count {
528 | font-size: 12px;
529 | margin-left: 12px;
530 | }
531 |
532 | .suite-count .icon {
533 | font-size: 10px;
534 | margin-right: -4px;
535 | }
536 |
537 | .raw-content {
538 | margin: 2px;
539 | }
540 |
541 | .pretty-content {
542 | margin: 20px;
543 | }
544 |
545 | .card,
546 | .card-header {
547 | box-shadow: none !important;
548 | }
549 |
550 | .card .card {
551 | border: solid 1px rgba(10, 10, 10, 0.1) !important;
552 | }
553 |
554 | .card-header {
555 | border-bottom: solid 1px #ccc !important;
556 | }
557 |
558 | .card.is-inactive .card-header {
559 | border: none !important;
560 | border-radius: 4px;
561 | }
562 |
563 | .options {
564 | border-radius: 0;
565 | }
566 |
567 | .options:last-child {
568 | border-radius: 4px;
569 | border-bottom: none;
570 | }
571 |
572 | @media (prefers-color-scheme: dark) {
573 | body,
574 | html {
575 | background-color: #051221;
576 | }
577 |
578 | .card {
579 | background-color: #2C3E50;
580 | color: white !important;
581 | /* box-shadow: 0 0.5em 1em -0.125em rgba(255, 255, 255, 0.1), 0 0px 0 1px rgba(255, 255, 255, 0.02) !important; */
582 | }
583 |
584 | .card .card {
585 | border-color: rgba(255, 255, 255, 0.1) !important;
586 | }
587 |
588 | .card-header svg path {
589 | color: white !important;
590 | fill: white !important;
591 | }
592 |
593 | .card-header {
594 | color: white !important;
595 | border-color: white !important;
596 | /* box-shadow: 0 0.125em 0.25em rgba(255, 255, 255, 0.1); */
597 | }
598 |
599 | .card-header-title {
600 | color: white !important;
601 | }
602 |
603 | .card-header:hover,
604 | .card-header:active,
605 | .card-header:focus {
606 | background-color: #17202a !important;
607 | }
608 |
609 | .table {
610 | background-color: #4d6784;
611 | color: white;
612 | }
613 |
614 | th {
615 | color: white !important;
616 | }
617 |
618 | .toggle {
619 | background-color: transparent;
620 | color: white !important;
621 | }
622 |
623 | .toggle-rail {
624 | background-color: #17202a;
625 | }
626 |
627 | .raw-content pre {
628 | background-color: #17202a;
629 | color: white !important;
630 | }
631 |
632 | .dropdown-content,
633 | .dropdown-item {
634 | background-color: #17202a;
635 | color: white;
636 | }
637 |
638 | .dropdown-item:hover,
639 | .dropdown-item:active,
640 | .dropdown-item:focus {
641 | background-color: #030d17 !important;
642 | color: white !important;
643 | }
644 |
645 | .input {
646 | background-color: transparent !important;
647 | color: white !important;
648 | border-color: white !important;
649 | }
650 |
651 | .input::placeholder {
652 | color: rgb(136, 136, 136) !important;
653 | }
654 |
655 | .options-inputs {
656 | color: white !important;
657 | }
658 |
659 | .test-options-toggle-row-label,
660 | .properties-options-toggle-label {
661 | color: white !important;
662 | }
663 |
664 | .test.is-empty>.card-header,
665 | .test>.card-header {
666 | background-color: #4B85FA !important;
667 | }
668 |
669 | .test.is-populated>.card-header:hover,
670 | .test.is-populated>.card-header:active,
671 | .test.is-populated>.card-header:focus {
672 | background-color: #2f5bb4 !important;
673 | }
674 |
675 | .test.is-passed>.card-header {
676 | background-color: #0DBF1F !important;
677 | }
678 |
679 | .test.is-populated.is-passed>.card-header:hover,
680 | .test.is-populated.is-passed>.card-header:active,
681 | .test.is-populated.is-passed>.card-header:focus {
682 | background-color: #25a132 !important;
683 | }
684 |
685 | .test.is-failure>.card-header {
686 | background-color: #B32010 !important;
687 | }
688 |
689 | .test.test.is-populated.is-failure>.card-header:hover,
690 | .test.test.is-populated.is-failure>.card-header:active,
691 | .test.test.is-populated.is-failure>.card-header:focus {
692 | background-color: #963126 !important;
693 | }
694 |
695 | .test.is-error>.card-header {
696 | background-color: #E5CB05 !important;
697 | }
698 |
699 |
700 | .test.is-populated.is-error>.card-header:hover,
701 | .test.is-populated.is-error>.card-header:active,
702 | .test.is-populated.is-error>.card-header:focus {
703 | background-color: #c7b319 !important;
704 | }
705 |
706 | .test.is-skipped>.card-header {
707 | background-color: #adb2b6 !important;
708 | }
709 |
710 | .test.is-populated.is-skipped>.card-header:hover,
711 | .test.is-populated.is-skipped>.card-header:active,
712 | .test.is-populated.is-skipped>.card-header:focus {
713 | background-color: #828588 !important;
714 | }
715 |
716 | .options,
717 | .options .card-header {
718 | background-color: transparent !important;
719 | }
720 | }
--------------------------------------------------------------------------------