@teambit/tester 0.0.566 → 0.0.570
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/exceptions/index.ts +2 -0
- package/exceptions/no-matching-components.ts +5 -0
- package/exceptions/no-test-files-found.ts +7 -0
- package/package-tar/teambit-tester-0.0.570.tgz +0 -0
- package/package.json +19 -34
- package/test.cmd.tsx +112 -0
- package/tester.service.tsx +161 -0
- package/tester.ui.runtime.tsx +42 -0
- package/tests.section.tsx +18 -0
- package/types/asset.d.ts +29 -0
- package/types/style.d.ts +42 -0
- package/ui/tests-page.tsx +134 -0
- package/utils/detect-spec-files.ts +12 -0
- package/utils/index.ts +1 -0
- package/utils/junit-generator.ts +35 -0
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teambit/tester",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.570",
|
|
4
4
|
"homepage": "https://bit.dev/teambit/defender/tester",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"componentId": {
|
|
7
7
|
"scope": "teambit.defender",
|
|
8
8
|
"name": "tester",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.570"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"chalk": "2.4.2",
|
|
@@ -24,24 +24,24 @@
|
|
|
24
24
|
"@babel/runtime": "7.12.18",
|
|
25
25
|
"core-js": "^3.0.0",
|
|
26
26
|
"@teambit/documenter.ui.heading": "4.1.1",
|
|
27
|
-
"@teambit/cli": "0.0.
|
|
28
|
-
"@teambit/logger": "0.0.
|
|
29
|
-
"@teambit/workspace": "0.0.
|
|
30
|
-
"@teambit/component": "0.0.
|
|
31
|
-
"@teambit/graphql": "0.0.
|
|
32
|
-
"@teambit/builder": "0.0.
|
|
33
|
-
"@teambit/dev-files": "0.0.
|
|
34
|
-
"@teambit/envs": "0.0.
|
|
35
|
-
"@teambit/tests-results": "0.0.
|
|
36
|
-
"@teambit/ui": "0.0.
|
|
37
|
-
"@teambit/compiler": "0.0.
|
|
38
|
-
"@teambit/bit-error": "0.0.
|
|
39
|
-
"@teambit/defender.ui.test-loader": "0.0.
|
|
40
|
-
"@teambit/defender.ui.test-table": "0.0.
|
|
27
|
+
"@teambit/cli": "0.0.395",
|
|
28
|
+
"@teambit/logger": "0.0.482",
|
|
29
|
+
"@teambit/workspace": "0.0.570",
|
|
30
|
+
"@teambit/component": "0.0.570",
|
|
31
|
+
"@teambit/graphql": "0.0.570",
|
|
32
|
+
"@teambit/builder": "0.0.570",
|
|
33
|
+
"@teambit/dev-files": "0.0.570",
|
|
34
|
+
"@teambit/envs": "0.0.570",
|
|
35
|
+
"@teambit/tests-results": "0.0.81",
|
|
36
|
+
"@teambit/ui": "0.0.570",
|
|
37
|
+
"@teambit/compiler": "0.0.570",
|
|
38
|
+
"@teambit/bit-error": "0.0.381",
|
|
39
|
+
"@teambit/defender.ui.test-loader": "0.0.474",
|
|
40
|
+
"@teambit/defender.ui.test-table": "0.0.477",
|
|
41
41
|
"@teambit/design.ui.alert-card": "0.0.13",
|
|
42
42
|
"@teambit/design.ui.empty-box": "0.0.352",
|
|
43
43
|
"@teambit/design.ui.separator": "0.0.351",
|
|
44
|
-
"@teambit/mdx.ui.mdx-layout": "0.0.
|
|
44
|
+
"@teambit/mdx.ui.mdx-layout": "0.0.478"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/react": "^17.0.8",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@apollo/client": "^3.0.0",
|
|
60
|
-
"@teambit/legacy": "1.0.
|
|
60
|
+
"@teambit/legacy": "1.0.181",
|
|
61
61
|
"react-dom": "^16.8.0 || ^17.0.0",
|
|
62
62
|
"react": "^16.8.0 || ^17.0.0"
|
|
63
63
|
},
|
|
@@ -85,27 +85,12 @@
|
|
|
85
85
|
"react": "-"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
|
-
"@teambit/legacy": "1.0.
|
|
88
|
+
"@teambit/legacy": "1.0.181",
|
|
89
89
|
"react-dom": "^16.8.0 || ^17.0.0",
|
|
90
90
|
"react": "^16.8.0 || ^17.0.0"
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
|
-
"files": [
|
|
95
|
-
"dist",
|
|
96
|
-
"!dist/tsconfig.tsbuildinfo",
|
|
97
|
-
"**/*.md",
|
|
98
|
-
"**/*.mdx",
|
|
99
|
-
"**/*.js",
|
|
100
|
-
"**/*.json",
|
|
101
|
-
"**/*.sass",
|
|
102
|
-
"**/*.scss",
|
|
103
|
-
"**/*.less",
|
|
104
|
-
"**/*.css",
|
|
105
|
-
"**/*.css",
|
|
106
|
-
"**/*.jpeg",
|
|
107
|
-
"**/*.gif"
|
|
108
|
-
],
|
|
109
94
|
"private": false,
|
|
110
95
|
"engines": {
|
|
111
96
|
"node": ">=12.22.0"
|
package/test.cmd.tsx
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Command, CommandOptions } from '@teambit/cli';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Logger } from '@teambit/logger';
|
|
4
|
+
import { Workspace } from '@teambit/workspace';
|
|
5
|
+
import { ConsumerNotFound } from '@teambit/legacy/dist/consumer/exceptions';
|
|
6
|
+
import { Timer } from '@teambit/legacy/dist/toolbox/timer';
|
|
7
|
+
import { Box, Text } from 'ink';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { NoMatchingComponents } from './exceptions';
|
|
10
|
+
|
|
11
|
+
import type { TesterMain } from './tester.main.runtime';
|
|
12
|
+
|
|
13
|
+
type TestFlags = {
|
|
14
|
+
watch: boolean;
|
|
15
|
+
debug: boolean;
|
|
16
|
+
all: boolean;
|
|
17
|
+
env?: string;
|
|
18
|
+
scope?: string;
|
|
19
|
+
junit?: string;
|
|
20
|
+
coverage?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class TestCmd implements Command {
|
|
24
|
+
name = 'test [pattern]';
|
|
25
|
+
description = 'test set of components in your workspace';
|
|
26
|
+
alias = 'at';
|
|
27
|
+
group = 'development';
|
|
28
|
+
shortDescription = '';
|
|
29
|
+
options = [
|
|
30
|
+
['w', 'watch', 'start the tester in watch mode.'],
|
|
31
|
+
['d', 'debug', 'start the tester in debug mode.'],
|
|
32
|
+
['a', 'all', 'test all components, not only new and modified'],
|
|
33
|
+
['', 'junit <filepath>', 'write tests results as JUnit XML format into the specified file path'],
|
|
34
|
+
['', 'coverage', 'show code coverage data'],
|
|
35
|
+
['e', 'env <id>', 'test only the given env'],
|
|
36
|
+
['s', 'scope <scope>', 'name of the scope to test'],
|
|
37
|
+
// TODO: we need to reduce this redundant casting every time.
|
|
38
|
+
] as CommandOptions;
|
|
39
|
+
|
|
40
|
+
constructor(private tester: TesterMain, private workspace: Workspace, private logger: Logger) {}
|
|
41
|
+
|
|
42
|
+
async render(
|
|
43
|
+
[userPattern]: [string],
|
|
44
|
+
{ watch = false, debug = false, all = false, env, scope, junit, coverage = false }: TestFlags
|
|
45
|
+
) {
|
|
46
|
+
this.logger.off();
|
|
47
|
+
const timer = Timer.create();
|
|
48
|
+
const scopeName = typeof scope === 'string' ? scope : undefined;
|
|
49
|
+
timer.start();
|
|
50
|
+
if (!this.workspace) throw new ConsumerNotFound();
|
|
51
|
+
|
|
52
|
+
const getPatternWithScope = () => {
|
|
53
|
+
if (!userPattern && !scope) return undefined;
|
|
54
|
+
const pattern = userPattern || '**';
|
|
55
|
+
return scopeName ? `${scopeName}/${pattern}` : pattern;
|
|
56
|
+
};
|
|
57
|
+
const patternWithScope = getPatternWithScope();
|
|
58
|
+
const components = await this.workspace.getComponentsByUserInput(all, patternWithScope, true);
|
|
59
|
+
if (!components.length) {
|
|
60
|
+
if (userPattern) throw new NoMatchingComponents(userPattern);
|
|
61
|
+
return {
|
|
62
|
+
code: 0,
|
|
63
|
+
data: (
|
|
64
|
+
<Box>
|
|
65
|
+
<Text bold>
|
|
66
|
+
no components found to test. use "--all" flag to test all components or specify the ids to test,
|
|
67
|
+
otherwise, only new and modified components will be tested{' '}
|
|
68
|
+
</Text>
|
|
69
|
+
</Box>
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.logger.console(
|
|
75
|
+
`testing total of ${components.length} components in workspace '${chalk.cyan(this.workspace.name)}'`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
let code = 0;
|
|
79
|
+
if (watch && !debug) {
|
|
80
|
+
await this.tester.watch(components, {
|
|
81
|
+
watch,
|
|
82
|
+
debug,
|
|
83
|
+
env,
|
|
84
|
+
coverage,
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
const tests = await this.tester.test(components, {
|
|
88
|
+
watch,
|
|
89
|
+
debug,
|
|
90
|
+
env,
|
|
91
|
+
junit,
|
|
92
|
+
coverage,
|
|
93
|
+
});
|
|
94
|
+
tests?.results?.forEach((test) => (test.data?.errors?.length ? (code = 1) : null));
|
|
95
|
+
}
|
|
96
|
+
const { seconds } = timer.stop();
|
|
97
|
+
|
|
98
|
+
if (watch) return <Box></Box>;
|
|
99
|
+
return {
|
|
100
|
+
code,
|
|
101
|
+
data: (
|
|
102
|
+
<Box>
|
|
103
|
+
<Text>tested </Text>
|
|
104
|
+
<Text color="cyan">{components.length} </Text>
|
|
105
|
+
<Text>components in </Text>
|
|
106
|
+
<Text color="cyan">{seconds} </Text>
|
|
107
|
+
<Text>seconds.</Text>
|
|
108
|
+
</Box>
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Logger } from '@teambit/logger';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Text, Newline } from 'ink';
|
|
5
|
+
import { EnvService, ExecutionContext, EnvDefinition } from '@teambit/envs';
|
|
6
|
+
import { ComponentMap } from '@teambit/component';
|
|
7
|
+
import { Workspace } from '@teambit/workspace';
|
|
8
|
+
import highlight from 'cli-highlight';
|
|
9
|
+
import { PubSub } from 'graphql-subscriptions';
|
|
10
|
+
import { DevFilesMain } from '@teambit/dev-files';
|
|
11
|
+
import { Tester, Tests, CallbackFn } from './tester';
|
|
12
|
+
import { TesterAspect } from './tester.aspect';
|
|
13
|
+
import { TesterOptions } from './tester.main.runtime';
|
|
14
|
+
import { detectTestFiles } from './utils';
|
|
15
|
+
import { NoTestFilesFound } from './exceptions';
|
|
16
|
+
|
|
17
|
+
const chalk = require('chalk');
|
|
18
|
+
|
|
19
|
+
export const OnTestsChanged = 'OnTestsChanged';
|
|
20
|
+
|
|
21
|
+
export type TesterDescriptor = {
|
|
22
|
+
/**
|
|
23
|
+
* id of the tester (e.g. jest/mocha)
|
|
24
|
+
*/
|
|
25
|
+
id: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* display name of the tester (e.g. Jest / Mocha)
|
|
29
|
+
*/
|
|
30
|
+
displayName: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* icon of the configured tester.
|
|
34
|
+
*/
|
|
35
|
+
icon: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* string containing the config for display.
|
|
39
|
+
*/
|
|
40
|
+
config: string;
|
|
41
|
+
|
|
42
|
+
version?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export class TesterService implements EnvService<Tests, TesterDescriptor> {
|
|
46
|
+
name = 'tester';
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
readonly workspace: Workspace,
|
|
50
|
+
/**
|
|
51
|
+
* regex used to identify which files to test.
|
|
52
|
+
*/
|
|
53
|
+
readonly patterns: string[],
|
|
54
|
+
|
|
55
|
+
private logger: Logger,
|
|
56
|
+
|
|
57
|
+
private pubsub: PubSub,
|
|
58
|
+
|
|
59
|
+
private devFiles: DevFilesMain
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
_callback: CallbackFn | undefined;
|
|
63
|
+
|
|
64
|
+
render(env: EnvDefinition) {
|
|
65
|
+
const descriptor = this.getDescriptor(env);
|
|
66
|
+
return (
|
|
67
|
+
<Text key={descriptor?.id}>
|
|
68
|
+
<Text color="cyan">configured tester: </Text>
|
|
69
|
+
<Text>
|
|
70
|
+
{descriptor?.id} ({descriptor?.displayName} @ {descriptor?.version})
|
|
71
|
+
</Text>
|
|
72
|
+
<Newline />
|
|
73
|
+
<Text underline color="cyan">
|
|
74
|
+
tester config:
|
|
75
|
+
</Text>
|
|
76
|
+
<Newline />
|
|
77
|
+
<Text>
|
|
78
|
+
{/* refactor a separate component which highlights for cli */}
|
|
79
|
+
{highlight(descriptor?.config || '', { language: 'javascript', ignoreIllegals: true })}
|
|
80
|
+
</Text>
|
|
81
|
+
<Newline />
|
|
82
|
+
</Text>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getDescriptor(environment: EnvDefinition) {
|
|
87
|
+
if (!environment.env.getTester) return undefined;
|
|
88
|
+
const tester: Tester = environment.env.getTester();
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: tester.id || '',
|
|
92
|
+
displayName: tester.displayName || '',
|
|
93
|
+
icon: tester.icon || '',
|
|
94
|
+
config: tester.displayConfig ? tester.displayConfig() : '',
|
|
95
|
+
version: tester.version ? tester.version() : '?',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onTestRunComplete(callback: CallbackFn) {
|
|
100
|
+
this._callback = callback;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async run(context: ExecutionContext, options: TesterOptions): Promise<Tests> {
|
|
104
|
+
const tester: Tester = context.env.getTester();
|
|
105
|
+
const specFiles = ComponentMap.as(context.components, (component) => {
|
|
106
|
+
return detectTestFiles(component, this.devFiles);
|
|
107
|
+
});
|
|
108
|
+
const testCount = specFiles.toArray().reduce((acc, [, specs]) => acc + specs.length, 0);
|
|
109
|
+
|
|
110
|
+
const componentWithTests = specFiles.toArray().reduce((acc: number, [, specs]) => {
|
|
111
|
+
if (specs.length > 0) acc += 1;
|
|
112
|
+
return acc;
|
|
113
|
+
}, 0);
|
|
114
|
+
|
|
115
|
+
if (testCount === 0 && !options.ui) throw new NoTestFilesFound(this.patterns.join(','));
|
|
116
|
+
|
|
117
|
+
if (!options.ui)
|
|
118
|
+
this.logger.console(`testing ${componentWithTests} components with environment ${chalk.cyan(context.id)}\n`);
|
|
119
|
+
|
|
120
|
+
const patterns = ComponentMap.as(context.components, (component) => {
|
|
121
|
+
const dir = this.workspace.componentDir(component.id);
|
|
122
|
+
const componentPatterns = this.devFiles.getDevPatterns(component, TesterAspect.id);
|
|
123
|
+
return componentPatterns.map((pattern: string) => ({ path: resolve(dir, pattern), relative: pattern }));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const testerContext = Object.assign(context, {
|
|
127
|
+
release: false,
|
|
128
|
+
specFiles,
|
|
129
|
+
patterns,
|
|
130
|
+
rootPath: this.workspace.path,
|
|
131
|
+
workspace: this.workspace,
|
|
132
|
+
debug: options.debug,
|
|
133
|
+
watch: options.watch,
|
|
134
|
+
ui: options.ui,
|
|
135
|
+
coverage: options.coverage,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (options.watch && options.ui && tester.watch) {
|
|
139
|
+
if (tester.onTestRunComplete) {
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
141
|
+
tester.onTestRunComplete((results) => {
|
|
142
|
+
if (this._callback) this._callback(results);
|
|
143
|
+
results.components.forEach((component) => {
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
145
|
+
this.pubsub.publish(OnTestsChanged, {
|
|
146
|
+
testsChanged: {
|
|
147
|
+
id: component.componentId.toString(),
|
|
148
|
+
testsResults: component.results,
|
|
149
|
+
loading: component.loading,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return tester.watch(testerContext);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return tester.test(testerContext);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
import { UIRuntime } from '@teambit/ui';
|
|
3
|
+
import { SlotRegistry, Slot } from '@teambit/harmony';
|
|
4
|
+
import { ComponentAspect, ComponentUI } from '@teambit/component';
|
|
5
|
+
import { TestsSection } from './tests.section';
|
|
6
|
+
import { TesterAspect } from './tester.aspect';
|
|
7
|
+
|
|
8
|
+
export type EmptyStateSlot = SlotRegistry<ComponentType>;
|
|
9
|
+
export class TesterUI {
|
|
10
|
+
static dependencies = [ComponentAspect];
|
|
11
|
+
|
|
12
|
+
static runtime = UIRuntime;
|
|
13
|
+
|
|
14
|
+
stageKey?: string;
|
|
15
|
+
|
|
16
|
+
constructor(private component: ComponentUI, private emptyStateSlot: EmptyStateSlot) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* register a new tester empty state. this allows to register a different empty state from each environment for example.
|
|
20
|
+
*/
|
|
21
|
+
registerEmptyState(emptyStateComponent: ComponentType) {
|
|
22
|
+
this.emptyStateSlot.register(emptyStateComponent);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static slots = [Slot.withType<ComponentType>()];
|
|
27
|
+
|
|
28
|
+
static async provider([component]: [ComponentUI], config, [emptyStateSlot]: [EmptyStateSlot]) {
|
|
29
|
+
const testerUi = new TesterUI(component, emptyStateSlot);
|
|
30
|
+
|
|
31
|
+
const section = new TestsSection(emptyStateSlot);
|
|
32
|
+
|
|
33
|
+
component.registerRoute(section.route);
|
|
34
|
+
component.registerNavigation(section.navigationLink, section.order);
|
|
35
|
+
|
|
36
|
+
return testerUi;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default TesterUI;
|
|
41
|
+
|
|
42
|
+
TesterAspect.addRuntime(TesterUI);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Section } from '@teambit/component';
|
|
3
|
+
import { TestsPage } from './ui/tests-page';
|
|
4
|
+
import { EmptyStateSlot } from './tester.ui.runtime';
|
|
5
|
+
|
|
6
|
+
export class TestsSection implements Section {
|
|
7
|
+
constructor(private emptyStateSlot: EmptyStateSlot) {}
|
|
8
|
+
|
|
9
|
+
route = {
|
|
10
|
+
path: '~tests',
|
|
11
|
+
children: <TestsPage emptyState={this.emptyStateSlot} />,
|
|
12
|
+
};
|
|
13
|
+
navigationLink = {
|
|
14
|
+
href: '~tests',
|
|
15
|
+
children: 'Tests',
|
|
16
|
+
};
|
|
17
|
+
order = 40;
|
|
18
|
+
}
|
package/types/asset.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare module '*.png' {
|
|
2
|
+
const value: any;
|
|
3
|
+
export = value;
|
|
4
|
+
}
|
|
5
|
+
declare module '*.svg' {
|
|
6
|
+
import type { FunctionComponent, SVGProps } from 'react';
|
|
7
|
+
|
|
8
|
+
export const ReactComponent: FunctionComponent<SVGProps<SVGSVGElement> & { title?: string }>;
|
|
9
|
+
const src: string;
|
|
10
|
+
export default src;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// @TODO Gilad
|
|
14
|
+
declare module '*.jpg' {
|
|
15
|
+
const value: any;
|
|
16
|
+
export = value;
|
|
17
|
+
}
|
|
18
|
+
declare module '*.jpeg' {
|
|
19
|
+
const value: any;
|
|
20
|
+
export = value;
|
|
21
|
+
}
|
|
22
|
+
declare module '*.gif' {
|
|
23
|
+
const value: any;
|
|
24
|
+
export = value;
|
|
25
|
+
}
|
|
26
|
+
declare module '*.bmp' {
|
|
27
|
+
const value: any;
|
|
28
|
+
export = value;
|
|
29
|
+
}
|
package/types/style.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
declare module '*.module.css' {
|
|
2
|
+
const classes: { readonly [key: string]: string };
|
|
3
|
+
export default classes;
|
|
4
|
+
}
|
|
5
|
+
declare module '*.module.scss' {
|
|
6
|
+
const classes: { readonly [key: string]: string };
|
|
7
|
+
export default classes;
|
|
8
|
+
}
|
|
9
|
+
declare module '*.module.sass' {
|
|
10
|
+
const classes: { readonly [key: string]: string };
|
|
11
|
+
export default classes;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module '*.module.less' {
|
|
15
|
+
const classes: { readonly [key: string]: string };
|
|
16
|
+
export default classes;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module '*.less' {
|
|
20
|
+
const classes: { readonly [key: string]: string };
|
|
21
|
+
export default classes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module '*.css' {
|
|
25
|
+
const classes: { readonly [key: string]: string };
|
|
26
|
+
export default classes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
declare module '*.sass' {
|
|
30
|
+
const classes: { readonly [key: string]: string };
|
|
31
|
+
export default classes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare module '*.scss' {
|
|
35
|
+
const classes: { readonly [key: string]: string };
|
|
36
|
+
export default classes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare module '*.mdx' {
|
|
40
|
+
const component: any;
|
|
41
|
+
export default component;
|
|
42
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useQuery, useSubscription, gql } from '@apollo/client';
|
|
2
|
+
import { ComponentContext } from '@teambit/component';
|
|
3
|
+
import { H1 } from '@teambit/documenter.ui.heading';
|
|
4
|
+
import { Separator } from '@teambit/design.ui.separator';
|
|
5
|
+
import { EmptyBox } from '@teambit/design.ui.empty-box';
|
|
6
|
+
import { MDXLayout } from '@teambit/mdx.ui.mdx-layout';
|
|
7
|
+
import { AlertCard } from '@teambit/design.ui.alert-card';
|
|
8
|
+
import { TestLoader } from '@teambit/defender.ui.test-loader';
|
|
9
|
+
import classNames from 'classnames';
|
|
10
|
+
import React, { HTMLAttributes, useContext } from 'react';
|
|
11
|
+
import { TestTable } from '@teambit/defender.ui.test-table';
|
|
12
|
+
import { EmptyStateSlot } from '../tester.ui.runtime';
|
|
13
|
+
import styles from './tests-page.module.scss';
|
|
14
|
+
|
|
15
|
+
const TESTS_SUBSCRIPTION_CHANGED = gql`
|
|
16
|
+
subscription OnTestsChanged($id: String!) {
|
|
17
|
+
testsChanged(id: $id) {
|
|
18
|
+
testsResults {
|
|
19
|
+
testFiles {
|
|
20
|
+
file
|
|
21
|
+
duration
|
|
22
|
+
pass
|
|
23
|
+
failed
|
|
24
|
+
pending
|
|
25
|
+
error {
|
|
26
|
+
failureMessage
|
|
27
|
+
}
|
|
28
|
+
tests {
|
|
29
|
+
ancestor
|
|
30
|
+
duration
|
|
31
|
+
status
|
|
32
|
+
name
|
|
33
|
+
error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const GET_COMPONENT = gql`
|
|
42
|
+
query($id: String!) {
|
|
43
|
+
getHost {
|
|
44
|
+
id # for GQL caching
|
|
45
|
+
getTests(id: $id) {
|
|
46
|
+
loading
|
|
47
|
+
testsResults {
|
|
48
|
+
testFiles {
|
|
49
|
+
file
|
|
50
|
+
duration
|
|
51
|
+
pass
|
|
52
|
+
failed
|
|
53
|
+
pending
|
|
54
|
+
error {
|
|
55
|
+
failureMessage
|
|
56
|
+
}
|
|
57
|
+
tests {
|
|
58
|
+
ancestor
|
|
59
|
+
duration
|
|
60
|
+
status
|
|
61
|
+
name
|
|
62
|
+
error
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
type TestsPageProps = {
|
|
72
|
+
emptyState: EmptyStateSlot;
|
|
73
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
74
|
+
|
|
75
|
+
export function TestsPage({ className, emptyState }: TestsPageProps) {
|
|
76
|
+
const component = useContext(ComponentContext);
|
|
77
|
+
const onTestsChanged = useSubscription(TESTS_SUBSCRIPTION_CHANGED, { variables: { id: component.id.toString() } });
|
|
78
|
+
const { data } = useQuery(GET_COMPONENT, {
|
|
79
|
+
variables: { id: component.id._legacy.name },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const testData = onTestsChanged.data?.testsChanged || data?.getHost?.getTests;
|
|
83
|
+
const testResults = testData?.testsResults?.testFiles;
|
|
84
|
+
|
|
85
|
+
// TODO: change loading EmptyBox
|
|
86
|
+
if (testData?.loading) return <TestLoader />;
|
|
87
|
+
|
|
88
|
+
const env = component.environment?.id;
|
|
89
|
+
const EmptyStateTemplate = emptyState.get(env || '');
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
(testResults === null || testData?.testsResults === null) &&
|
|
93
|
+
component.host === 'teambit.workspace/workspace' &&
|
|
94
|
+
EmptyStateTemplate
|
|
95
|
+
) {
|
|
96
|
+
return (
|
|
97
|
+
<div className={classNames(styles.testsPage, className)}>
|
|
98
|
+
<div>
|
|
99
|
+
<H1 className={styles.title}>Tests</H1>
|
|
100
|
+
<Separator isPresentational className={styles.separator} />
|
|
101
|
+
<AlertCard
|
|
102
|
+
level="info"
|
|
103
|
+
title="There are no
|
|
104
|
+
tests for this Component. Learn how to add tests:"
|
|
105
|
+
>
|
|
106
|
+
<MDXLayout>
|
|
107
|
+
<EmptyStateTemplate />
|
|
108
|
+
</MDXLayout>
|
|
109
|
+
</AlertCard>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (testResults === null || testData?.testsResults === null) {
|
|
116
|
+
return (
|
|
117
|
+
<EmptyBox
|
|
118
|
+
title="This component doesn’t have any tests."
|
|
119
|
+
linkText="Learn how to add tests to your components"
|
|
120
|
+
link="https://harmony-docs.bit.dev/testing/overview/"
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className={classNames(styles.testsPage, className)}>
|
|
127
|
+
<div>
|
|
128
|
+
<H1 className={styles.title}>Tests</H1>
|
|
129
|
+
<Separator isPresentational className={styles.separator} />
|
|
130
|
+
<TestTable testResults={testResults} className={styles.testBlock} />
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Component } from '@teambit/component';
|
|
2
|
+
import { DevFilesMain } from '@teambit/dev-files';
|
|
3
|
+
import { TesterAspect } from '../tester.aspect';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* detect test files in components
|
|
7
|
+
*/
|
|
8
|
+
export function detectTestFiles(component: Component, devFiles: DevFilesMain) {
|
|
9
|
+
const files = devFiles.getDevFiles(component);
|
|
10
|
+
const testFiles = files.get(TesterAspect.id);
|
|
11
|
+
return component.state.filesystem.files.filter((file) => testFiles.includes(file.relative));
|
|
12
|
+
}
|
package/utils/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { detectTestFiles } from './detect-spec-files';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import junitReportBuilder from 'junit-report-builder';
|
|
2
|
+
import stripAnsi from 'strip-ansi';
|
|
3
|
+
import { ComponentsResults } from '../tester';
|
|
4
|
+
|
|
5
|
+
export function testsResultsToJUnitFormat(components: ComponentsResults[]): string {
|
|
6
|
+
const builder = junitReportBuilder.newBuilder();
|
|
7
|
+
components.forEach((compResult) => {
|
|
8
|
+
const suite = builder.testSuite().name(compResult.componentId.toString());
|
|
9
|
+
if (compResult.results?.start) {
|
|
10
|
+
suite.timestamp(new Date(compResult.results?.start).toISOString());
|
|
11
|
+
}
|
|
12
|
+
compResult.results?.testFiles.forEach((testFile) => {
|
|
13
|
+
if (testFile.error?.error) {
|
|
14
|
+
const testCase = suite.testCase().className(testFile.file).name(testFile.file);
|
|
15
|
+
testCase.error(stripAnsi(testFile.error.error as string));
|
|
16
|
+
}
|
|
17
|
+
testFile.tests.forEach((test) => {
|
|
18
|
+
const testCase = suite.testCase().className(testFile.file).name(test.name);
|
|
19
|
+
if (test.error) {
|
|
20
|
+
testCase.error(stripAnsi(test.error));
|
|
21
|
+
}
|
|
22
|
+
if (test.failure) {
|
|
23
|
+
testCase.failure(stripAnsi(test.failure));
|
|
24
|
+
}
|
|
25
|
+
if (test.status === 'skipped' || test.status === 'pending') {
|
|
26
|
+
testCase.skipped();
|
|
27
|
+
}
|
|
28
|
+
if (test.duration) {
|
|
29
|
+
testCase.time(test.duration / 1000);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
return builder.build();
|
|
35
|
+
}
|