@react-native-harness/jest 1.0.0 → 1.1.0-rc.2
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/dist/__tests__/crash-supervisor.test.d.ts +2 -0
- package/dist/__tests__/crash-supervisor.test.d.ts.map +1 -0
- package/dist/__tests__/crash-supervisor.test.js +151 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +54 -0
- package/dist/__tests__/harness-cache.test.d.ts +2 -0
- package/dist/__tests__/harness-cache.test.d.ts.map +1 -0
- package/dist/__tests__/harness-cache.test.js +51 -0
- package/dist/__tests__/harness.test.d.ts +2 -0
- package/dist/__tests__/harness.test.d.ts.map +1 -0
- package/dist/__tests__/harness.test.js +399 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +42 -0
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/crash-supervisor.d.ts +21 -0
- package/dist/crash-supervisor.d.ts.map +1 -0
- package/dist/crash-supervisor.js +210 -0
- package/dist/errors.d.ts +9 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +43 -8
- package/dist/harness.d.ts +30 -8
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +440 -98
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +205 -57
- package/dist/logs.d.ts +1 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +3 -0
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +13 -8
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +18 -6
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/src/__tests__/crash-supervisor.test.ts +200 -0
- package/src/__tests__/errors.test.ts +70 -0
- package/src/__tests__/harness-cache.test.ts +74 -0
- package/src/__tests__/harness.test.ts +547 -0
- package/src/__tests__/index.test.ts +61 -0
- package/src/cli-args.ts +6 -0
- package/src/crash-supervisor.ts +321 -0
- package/src/env.d.ts +1 -1
- package/src/errors.ts +68 -8
- package/src/harness.ts +566 -120
- package/src/index.ts +276 -66
- package/src/logs.ts +4 -0
- package/src/run.ts +20 -10
- package/src/setup.ts +22 -6
- package/tsconfig.json +3 -3
- package/tsconfig.lib.json +6 -6
- package/dist/cli-args.test.d.ts +0 -2
- package/dist/cli-args.test.d.ts.map +0 -1
- package/dist/cli-args.test.js +0 -1
- package/dist/crash-monitor.d.ts +0 -16
- package/dist/crash-monitor.d.ts.map +0 -1
- package/dist/crash-monitor.js +0 -75
- package/dist/event-mapper.d.ts +0 -16
- package/dist/event-mapper.d.ts.map +0 -1
- package/dist/event-mapper.js +0 -120
- package/dist/global-setup.d.ts +0 -3
- package/dist/global-setup.d.ts.map +0 -1
- package/dist/global-setup.js +0 -60
- package/dist/global-teardown.d.ts +0 -3
- package/dist/global-teardown.d.ts.map +0 -1
- package/dist/global-teardown.js +0 -11
- package/dist/harness-reporter.d.ts +0 -8
- package/dist/harness-reporter.d.ts.map +0 -1
- package/dist/harness-reporter.js +0 -12
- package/dist/jest-options.d.ts +0 -2
- package/dist/jest-options.d.ts.map +0 -1
- package/dist/jest-options.js +0 -1
- package/dist/types.d.ts +0 -17
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
- package/src/crash-monitor.ts +0 -109
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AppMonitor,
|
|
3
|
+
type AppCrashDetails,
|
|
4
|
+
type AppMonitorEvent,
|
|
5
|
+
type AppMonitorListener,
|
|
6
|
+
type HarnessPlatformRunner,
|
|
7
|
+
} from '@react-native-harness/platforms';
|
|
8
|
+
import {
|
|
9
|
+
NativeCrashError,
|
|
10
|
+
type NativeCrashDetails,
|
|
11
|
+
type NativeCrashPhase,
|
|
12
|
+
} from './errors.js';
|
|
13
|
+
import { logger } from '@react-native-harness/tools';
|
|
14
|
+
|
|
15
|
+
const crashLogger = logger.child('crash');
|
|
16
|
+
|
|
17
|
+
export type CrashSupervisorState =
|
|
18
|
+
| 'idle'
|
|
19
|
+
| 'launching'
|
|
20
|
+
| 'ready'
|
|
21
|
+
| 'running'
|
|
22
|
+
| 'disposing';
|
|
23
|
+
|
|
24
|
+
export type CrashSupervisor = {
|
|
25
|
+
setActiveTestFile: (testFilePath: string | null) => void;
|
|
26
|
+
beginLaunch: (testFilePath: string) => void;
|
|
27
|
+
markReady: () => void;
|
|
28
|
+
beginTestRun: (testFilePath: string) => void;
|
|
29
|
+
stop: () => Promise<void>;
|
|
30
|
+
start: () => Promise<void>;
|
|
31
|
+
waitForCrash: (testFilePath: string) => Promise<never>;
|
|
32
|
+
isReady: () => boolean;
|
|
33
|
+
cancelCrashWaiters: () => void;
|
|
34
|
+
reset: () => void;
|
|
35
|
+
dispose: () => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type CrashSupervisorOptions = {
|
|
39
|
+
appMonitor: AppMonitor;
|
|
40
|
+
platformRunner: HarnessPlatformRunner;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type CrashDetailsProvider = {
|
|
44
|
+
getCrashDetails?: (
|
|
45
|
+
options: {
|
|
46
|
+
processName?: string;
|
|
47
|
+
pid?: number;
|
|
48
|
+
occurredAt: number;
|
|
49
|
+
}
|
|
50
|
+
) => Promise<AppCrashDetails | null>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getCrashPhase = (state: CrashSupervisorState): NativeCrashPhase =>
|
|
54
|
+
state === 'running' ? 'execution' : 'startup';
|
|
55
|
+
|
|
56
|
+
const mergeCrashDetails = (
|
|
57
|
+
phase: NativeCrashPhase,
|
|
58
|
+
initialDetails?: AppCrashDetails,
|
|
59
|
+
enrichedDetails?: AppCrashDetails | null,
|
|
60
|
+
fallbackSummary?: string
|
|
61
|
+
): NativeCrashDetails => ({
|
|
62
|
+
phase,
|
|
63
|
+
source: enrichedDetails?.source ?? initialDetails?.source,
|
|
64
|
+
summary:
|
|
65
|
+
enrichedDetails?.summary ?? initialDetails?.summary ?? fallbackSummary,
|
|
66
|
+
signal: enrichedDetails?.signal ?? initialDetails?.signal,
|
|
67
|
+
exceptionType: enrichedDetails?.exceptionType ?? initialDetails?.exceptionType,
|
|
68
|
+
processName: enrichedDetails?.processName ?? initialDetails?.processName,
|
|
69
|
+
pid: enrichedDetails?.pid ?? initialDetails?.pid,
|
|
70
|
+
stackTrace: enrichedDetails?.stackTrace ?? initialDetails?.stackTrace,
|
|
71
|
+
rawLines: enrichedDetails?.rawLines ?? initialDetails?.rawLines,
|
|
72
|
+
artifactType: enrichedDetails?.artifactType ?? initialDetails?.artifactType,
|
|
73
|
+
artifactPath: enrichedDetails?.artifactPath ?? initialDetails?.artifactPath,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const createCrashSupervisor = ({
|
|
77
|
+
appMonitor,
|
|
78
|
+
platformRunner,
|
|
79
|
+
}: CrashSupervisorOptions): CrashSupervisor => {
|
|
80
|
+
let state: CrashSupervisorState = 'idle';
|
|
81
|
+
let activeTestFilePath: string | null = null;
|
|
82
|
+
let crashRejectors = new Set<(error: NativeCrashError) => void>();
|
|
83
|
+
let disposed = false;
|
|
84
|
+
let monitoring = true;
|
|
85
|
+
let isResolvingCrash = false;
|
|
86
|
+
|
|
87
|
+
const getCrashDetailsProvider = (): CrashDetailsProvider | null => {
|
|
88
|
+
if ('getCrashDetails' in appMonitor) {
|
|
89
|
+
return appMonitor as AppMonitor & CrashDetailsProvider;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (platformRunner.getCrashDetails) {
|
|
93
|
+
return platformRunner;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const rejectCrashWaiters = (testFilePath: string, details: NativeCrashDetails) => {
|
|
100
|
+
const error = new NativeCrashError(testFilePath, details);
|
|
101
|
+
|
|
102
|
+
for (const reject of crashRejectors) {
|
|
103
|
+
reject(error);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
crashRejectors = new Set();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleCrash = async (reason: string, details?: AppCrashDetails) => {
|
|
110
|
+
if (
|
|
111
|
+
isResolvingCrash ||
|
|
112
|
+
(state !== 'launching' && state !== 'running' && state !== 'ready')
|
|
113
|
+
) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!activeTestFilePath) {
|
|
118
|
+
crashLogger.debug('ignoring crash signal without active test: %s', reason);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
isResolvingCrash = true;
|
|
123
|
+
crashLogger.debug(
|
|
124
|
+
'native crash detected during %s (state=%s reason=%s)',
|
|
125
|
+
activeTestFilePath,
|
|
126
|
+
state,
|
|
127
|
+
reason || '(none)'
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
for (const line of details?.rawLines ?? []) {
|
|
131
|
+
crashLogger.debug('%s', line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const phase = getCrashPhase(state);
|
|
135
|
+
const testFilePath = activeTestFilePath;
|
|
136
|
+
state = 'idle';
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const enrichedDetails = await getCrashDetailsProvider()?.getCrashDetails?.({
|
|
140
|
+
processName: details?.processName,
|
|
141
|
+
pid: details?.pid,
|
|
142
|
+
occurredAt: Date.now(),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const mergedDetails = mergeCrashDetails(phase, details, enrichedDetails, reason);
|
|
146
|
+
crashLogger.debug('crash details: %o', {
|
|
147
|
+
phase: mergedDetails.phase,
|
|
148
|
+
source: mergedDetails.source,
|
|
149
|
+
summary: mergedDetails.summary,
|
|
150
|
+
signal: mergedDetails.signal,
|
|
151
|
+
exceptionType: mergedDetails.exceptionType,
|
|
152
|
+
processName: mergedDetails.processName,
|
|
153
|
+
pid: mergedDetails.pid,
|
|
154
|
+
});
|
|
155
|
+
rejectCrashWaiters(testFilePath, mergedDetails);
|
|
156
|
+
} finally {
|
|
157
|
+
isResolvingCrash = false;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const confirmCrash = async (reason: string, details?: AppCrashDetails) => {
|
|
162
|
+
if (disposed || !monitoring || state === 'disposing') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const isRunning = await platformRunner.isAppRunning();
|
|
168
|
+
|
|
169
|
+
if (!isRunning) {
|
|
170
|
+
void handleCrash(reason, details);
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
crashLogger.debug('crash confirmation failed', error);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const appMonitorListener: AppMonitorListener = (event: AppMonitorEvent) => {
|
|
178
|
+
if (disposed || !monitoring) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (event.type === 'app_started') {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (event.type === 'app_exited') {
|
|
187
|
+
const details = {
|
|
188
|
+
source: event.crashDetails?.source ?? event.source,
|
|
189
|
+
summary: event.crashDetails?.summary,
|
|
190
|
+
signal: event.crashDetails?.signal,
|
|
191
|
+
exceptionType: event.crashDetails?.exceptionType,
|
|
192
|
+
processName: event.crashDetails?.processName,
|
|
193
|
+
pid: event.crashDetails?.pid ?? event.pid,
|
|
194
|
+
stackTrace: event.crashDetails?.stackTrace,
|
|
195
|
+
rawLines:
|
|
196
|
+
event.crashDetails?.rawLines ??
|
|
197
|
+
(event.line ? [event.line] : undefined),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (event.isConfirmed ?? event.source === 'polling') {
|
|
201
|
+
void handleCrash('', details);
|
|
202
|
+
} else {
|
|
203
|
+
void confirmCrash('', details);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (event.type === 'possible_crash') {
|
|
209
|
+
const details = {
|
|
210
|
+
source: event.crashDetails?.source ?? event.source,
|
|
211
|
+
summary: event.crashDetails?.summary,
|
|
212
|
+
signal: event.crashDetails?.signal,
|
|
213
|
+
exceptionType: event.crashDetails?.exceptionType,
|
|
214
|
+
processName: event.crashDetails?.processName,
|
|
215
|
+
pid: event.crashDetails?.pid ?? event.pid,
|
|
216
|
+
stackTrace: event.crashDetails?.stackTrace,
|
|
217
|
+
rawLines:
|
|
218
|
+
event.crashDetails?.rawLines ??
|
|
219
|
+
(event.line ? [event.line] : undefined),
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (event.isConfirmed) {
|
|
223
|
+
void handleCrash(
|
|
224
|
+
`possible crash signal (${event.source ?? 'unknown'})`,
|
|
225
|
+
details
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
void confirmCrash(
|
|
229
|
+
`possible crash signal (${event.source ?? 'unknown'})`,
|
|
230
|
+
details
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
appMonitor.addListener(appMonitorListener);
|
|
237
|
+
|
|
238
|
+
const setActiveTestFile = (testFilePath: string | null) => {
|
|
239
|
+
activeTestFilePath = testFilePath;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const beginLaunch = (testFilePath: string) => {
|
|
243
|
+
activeTestFilePath = testFilePath;
|
|
244
|
+
state = 'launching';
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const markReady = () => {
|
|
248
|
+
if (state !== 'disposing') {
|
|
249
|
+
state = 'ready';
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const beginTestRun = (testFilePath: string) => {
|
|
254
|
+
activeTestFilePath = testFilePath;
|
|
255
|
+
state = 'running';
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const stop = async () => {
|
|
259
|
+
monitoring = false;
|
|
260
|
+
await appMonitor.stop();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const start = async () => {
|
|
264
|
+
monitoring = true;
|
|
265
|
+
await appMonitor.start();
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const waitForCrash = (testFilePath: string): Promise<never> =>
|
|
269
|
+
new Promise<never>((_, reject) => {
|
|
270
|
+
if (disposed) {
|
|
271
|
+
reject(
|
|
272
|
+
new NativeCrashError(testFilePath, {
|
|
273
|
+
phase: 'startup',
|
|
274
|
+
source: 'polling',
|
|
275
|
+
summary: 'Crash supervisor disposed while waiting for app startup.',
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
crashRejectors.add(reject);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const isReady = () => state === 'ready' || state === 'running';
|
|
285
|
+
|
|
286
|
+
const cancelCrashWaiters = () => {
|
|
287
|
+
crashRejectors = new Set();
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const reset = () => {
|
|
291
|
+
cancelCrashWaiters();
|
|
292
|
+
isResolvingCrash = false;
|
|
293
|
+
if (state !== 'disposing') {
|
|
294
|
+
state = 'idle';
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const dispose = async () => {
|
|
299
|
+
disposed = true;
|
|
300
|
+
monitoring = false;
|
|
301
|
+
state = 'disposing';
|
|
302
|
+
crashRejectors = new Set();
|
|
303
|
+
isResolvingCrash = false;
|
|
304
|
+
appMonitor.removeListener(appMonitorListener);
|
|
305
|
+
await appMonitor.dispose();
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
setActiveTestFile,
|
|
310
|
+
beginLaunch,
|
|
311
|
+
markReady,
|
|
312
|
+
beginTestRun,
|
|
313
|
+
stop,
|
|
314
|
+
start,
|
|
315
|
+
waitForCrash,
|
|
316
|
+
isReady,
|
|
317
|
+
cancelCrashWaiters,
|
|
318
|
+
reset,
|
|
319
|
+
dispose,
|
|
320
|
+
};
|
|
321
|
+
};
|
package/src/env.d.ts
CHANGED
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { HarnessError } from '@react-native-harness/tools';
|
|
2
|
+
import type { AppCrashDetails } from '@react-native-harness/platforms';
|
|
3
|
+
export {
|
|
4
|
+
StartupStallError,
|
|
5
|
+
type StartupStallCode,
|
|
6
|
+
type StartupStallDetails,
|
|
7
|
+
} from '@react-native-harness/bundler-metro';
|
|
2
8
|
|
|
3
9
|
export class NoRunnerSpecifiedError extends HarnessError {
|
|
4
10
|
constructor() {
|
|
@@ -21,22 +27,76 @@ export class InitializationTimeoutError extends HarnessError {
|
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
export type NativeCrashPhase = 'startup' | 'execution';
|
|
31
|
+
|
|
32
|
+
export type NativeCrashDetails = AppCrashDetails & {
|
|
33
|
+
phase: NativeCrashPhase;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const buildNativeCrashMessage = ({
|
|
37
|
+
phase,
|
|
38
|
+
summary,
|
|
39
|
+
signal,
|
|
40
|
+
exceptionType,
|
|
41
|
+
processName,
|
|
42
|
+
pid,
|
|
43
|
+
stackTrace,
|
|
44
|
+
artifactType,
|
|
45
|
+
}: NativeCrashDetails) => {
|
|
46
|
+
const lines = [
|
|
47
|
+
phase === 'startup'
|
|
48
|
+
? 'The native app crashed while preparing to run this test file.'
|
|
49
|
+
: 'The native app crashed during test execution.',
|
|
50
|
+
];
|
|
51
|
+
const hasCrashBlock = summary?.includes('\n') ?? false;
|
|
52
|
+
const shouldRenderSummary =
|
|
53
|
+
Boolean(summary) &&
|
|
54
|
+
!(
|
|
55
|
+
!hasCrashBlock &&
|
|
56
|
+
artifactType === 'ios-crash-report'
|
|
29
57
|
);
|
|
30
|
-
|
|
58
|
+
|
|
59
|
+
if (shouldRenderSummary && summary) {
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push(summary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!hasCrashBlock && signal) {
|
|
65
|
+
lines.push(`Signal: ${signal}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!hasCrashBlock && exceptionType) {
|
|
69
|
+
lines.push(`Exception: ${exceptionType}`);
|
|
31
70
|
}
|
|
32
|
-
|
|
71
|
+
|
|
72
|
+
if (!hasCrashBlock && processName && pid !== undefined) {
|
|
73
|
+
lines.push(`Process: ${processName} (pid ${pid})`);
|
|
74
|
+
} else if (!hasCrashBlock && processName) {
|
|
75
|
+
lines.push(`Process: ${processName}`);
|
|
76
|
+
} else if (!hasCrashBlock && pid !== undefined) {
|
|
77
|
+
lines.push(`PID: ${pid}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!hasCrashBlock && stackTrace && stackTrace.length > 0) {
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(...stackTrace.map((line) => ` ${line}`));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
};
|
|
33
87
|
|
|
34
88
|
export class NativeCrashError extends HarnessError {
|
|
35
89
|
constructor(
|
|
36
90
|
public readonly testFilePath: string,
|
|
91
|
+
public readonly details: NativeCrashDetails,
|
|
37
92
|
public readonly lastKnownTest?: string
|
|
38
93
|
) {
|
|
39
|
-
super(
|
|
94
|
+
super(buildNativeCrashMessage(details));
|
|
40
95
|
this.name = 'NativeCrashError';
|
|
96
|
+
this.stack = `${this.name}: ${this.message.split('\n')[0]}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get phase() {
|
|
100
|
+
return this.details.phase;
|
|
41
101
|
}
|
|
42
102
|
}
|