@esimplicity/stack-tests 0.1.0

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.
@@ -0,0 +1,540 @@
1
+ import * as playwright_bdd from 'playwright-bdd';
2
+ export { test as baseTest } from 'playwright-bdd';
3
+ import * as _playwright_test from '@playwright/test';
4
+ import { APIResponse, PlaywrightTestArgs, PlaywrightWorkerArgs, APIRequestContext, Page } from '@playwright/test';
5
+ export { registerApiAssertionSteps, registerApiAuthSteps, registerApiHttpSteps, registerApiSteps, registerHybridSteps, registerHybridSuite, registerSharedCleanupSteps, registerSharedSteps, registerSharedVarSteps, registerTuiBasicSteps, registerTuiSteps, registerTuiWizardSteps, registerUiBasicSteps, registerUiSteps, registerWizardSteps } from './steps/index.js';
6
+
7
+ type CleanupItem = {
8
+ method: 'DELETE' | 'POST' | 'PATCH' | 'PUT';
9
+ path: string;
10
+ headers?: Record<string, string>;
11
+ };
12
+ type World = {
13
+ vars: Record<string, string>;
14
+ headers: Record<string, string>;
15
+ cleanup: CleanupItem[];
16
+ skipCleanup?: boolean;
17
+ lastResponse?: APIResponse;
18
+ lastStatus?: number;
19
+ lastText?: string;
20
+ lastJson?: unknown;
21
+ lastHeaders?: Record<string, string>;
22
+ lastContentType?: string;
23
+ };
24
+ declare function initWorld(): World;
25
+
26
+ type ApiResult = {
27
+ status: number;
28
+ text: string;
29
+ json?: unknown;
30
+ headers: Record<string, string>;
31
+ contentType?: string;
32
+ response: APIResponse;
33
+ };
34
+ type ApiMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
35
+ interface ApiPort {
36
+ sendJson(method: ApiMethod, path: string, body?: unknown, headers?: Record<string, string>): Promise<ApiResult>;
37
+ sendForm(method: 'POST' | 'PUT' | 'PATCH', path: string, form: Record<string, string>, headers?: Record<string, string>): Promise<ApiResult>;
38
+ }
39
+
40
+ type UiClickMode = 'click' | 'dispatch click' | 'force click' | 'force dispatch click';
41
+ type UiInputMode = 'type' | 'fill' | 'choose';
42
+ type UiUrlAssertMode = 'contains' | 'doesntContain' | 'equals';
43
+ type UiLocatorMethod = 'text' | 'label' | 'placeholder' | 'role' | 'test ID' | 'alternative text' | 'title' | 'locator';
44
+ type UiElementState = 'visible' | 'hidden' | 'editable' | 'disabled' | 'enabled' | 'read-only';
45
+ interface UiPort {
46
+ goto(path: string): Promise<void>;
47
+ clickButton(name: string): Promise<void>;
48
+ clickLink(name: string): Promise<void>;
49
+ fillPlaceholder(placeholder: string, value: string): Promise<void>;
50
+ fillLabel(label: string, value: string): Promise<void>;
51
+ expectText(text: string): Promise<void>;
52
+ expectUrlContains(part: string): Promise<void>;
53
+ goBack(): Promise<void>;
54
+ reload(): Promise<void>;
55
+ waitSeconds(seconds: number): Promise<void>;
56
+ waitForPageLoad(): Promise<void>;
57
+ getCurrentUrl(): Promise<string>;
58
+ zoomTo(scale: number): Promise<void>;
59
+ typeText(text: string): Promise<void>;
60
+ pressKey(key: string): Promise<void>;
61
+ clickElementThatContains(clickMode: UiClickMode, elementType: string, text: string): Promise<void>;
62
+ clickElementWith(clickMode: UiClickMode, ordinal: string, text: string, method: UiLocatorMethod): Promise<void>;
63
+ fillDropdown(value: string, dropdownLabel: string): Promise<void>;
64
+ inputInElement(action: UiInputMode, value: string, ordinal: string, text: string, method: UiLocatorMethod): Promise<void>;
65
+ expectUrl(mode: UiUrlAssertMode, expected: string): Promise<void>;
66
+ expectNewTabUrl(mode: UiUrlAssertMode, expected: string): Promise<void>;
67
+ expectElementWithTextVisible(elementType: string, text: string, shouldBeVisible: boolean): Promise<void>;
68
+ expectElementState(ordinal: string, text: string, method: UiLocatorMethod, state: UiElementState): Promise<void>;
69
+ expectElementStateWithin(ordinal: string, text: string, method: UiLocatorMethod, state: UiElementState, seconds: number): Promise<void>;
70
+ }
71
+
72
+ interface AuthPort {
73
+ apiLoginAsAdmin(world: World): Promise<void>;
74
+ apiLoginAsUser(world: World): Promise<void>;
75
+ apiSetBearer(world: World, token: string): void;
76
+ uiLoginAsAdmin(world: World): Promise<void>;
77
+ uiLoginAsUser(world: World): Promise<void>;
78
+ }
79
+
80
+ interface CleanupPort {
81
+ registerFromVar(world: World, varName: string, id: unknown, meta?: unknown): void;
82
+ }
83
+
84
+ /**
85
+ * TUI Port Interface
86
+ *
87
+ * Defines the contract for terminal user interface testing operations.
88
+ * Aligned with UiPort patterns for consistency across testing approaches.
89
+ */
90
+ /** Keyboard modifier keys for key combinations */
91
+ type TuiKeyModifiers = {
92
+ ctrl?: boolean;
93
+ alt?: boolean;
94
+ shift?: boolean;
95
+ meta?: boolean;
96
+ };
97
+ /** Options for wait operations */
98
+ type TuiWaitOptions = {
99
+ /** Maximum time to wait in milliseconds */
100
+ timeout?: number;
101
+ /** Polling interval in milliseconds */
102
+ interval?: number;
103
+ };
104
+ /** Captured screen state */
105
+ type TuiScreenCapture = {
106
+ /** Full screen text content */
107
+ text: string;
108
+ /** Screen content split by lines */
109
+ lines: string[];
110
+ /** Timestamp when capture was taken */
111
+ timestamp: number;
112
+ /** Terminal dimensions */
113
+ size: {
114
+ cols: number;
115
+ rows: number;
116
+ };
117
+ };
118
+ /** Result of snapshot comparison */
119
+ type TuiSnapshotResult = {
120
+ /** Whether the snapshot matched */
121
+ pass: boolean;
122
+ /** Diff output if snapshot didn't match */
123
+ diff?: string;
124
+ /** Path to the snapshot file */
125
+ snapshotPath?: string;
126
+ };
127
+ /** Mouse button types */
128
+ type TuiMouseButton = 'left' | 'right' | 'middle';
129
+ /** Mouse event types */
130
+ type TuiMouseEventType = 'click' | 'down' | 'up' | 'drag' | 'scroll';
131
+ /** Mouse event configuration */
132
+ type TuiMouseEvent = {
133
+ type: TuiMouseEventType;
134
+ position: {
135
+ x: number;
136
+ y: number;
137
+ };
138
+ button?: TuiMouseButton;
139
+ };
140
+ /** Configuration for TUI adapter initialization */
141
+ type TuiConfig = {
142
+ /** Command to start the TUI application (e.g., ['node', 'cli.js']) */
143
+ command: string[];
144
+ /** Terminal size (defaults to 80x24) */
145
+ size?: {
146
+ cols: number;
147
+ rows: number;
148
+ };
149
+ /** Working directory for the command */
150
+ cwd?: string;
151
+ /** Environment variables */
152
+ env?: Record<string, string>;
153
+ /** Enable debug output */
154
+ debug?: boolean;
155
+ /** Directory for snapshot storage */
156
+ snapshotDir?: string;
157
+ /** Shell to use (defaults to system shell) */
158
+ shell?: string;
159
+ };
160
+ /**
161
+ * Port interface for terminal user interface testing.
162
+ *
163
+ * This interface follows the same patterns as UiPort to enable:
164
+ * - Consistent step definitions across UI and TUI tests
165
+ * - Easy mental model for test authors
166
+ * - Potential for shared/hybrid testing scenarios
167
+ */
168
+ interface TuiPort {
169
+ /**
170
+ * Start the TUI application.
171
+ * Creates a new terminal session and launches the configured command.
172
+ */
173
+ start(): Promise<void>;
174
+ /**
175
+ * Stop the TUI application.
176
+ * Terminates the terminal session and cleans up resources.
177
+ */
178
+ stop(): Promise<void>;
179
+ /**
180
+ * Restart the TUI application.
181
+ * Equivalent to stop() followed by start().
182
+ */
183
+ restart(): Promise<void>;
184
+ /**
185
+ * Check if the TUI application is currently running.
186
+ */
187
+ isRunning(): boolean;
188
+ /**
189
+ * Type text into the terminal.
190
+ * Similar to UiPort.typeText - sends characters one by one.
191
+ * @param text - The text to type
192
+ * @param options - Optional typing options
193
+ */
194
+ typeText(text: string, options?: {
195
+ delay?: number;
196
+ }): Promise<void>;
197
+ /**
198
+ * Press a keyboard key, optionally with modifiers.
199
+ * Similar to UiPort.pressKey.
200
+ * @param key - Key name (e.g., 'enter', 'tab', 'up', 'down', 'f1')
201
+ * @param modifiers - Optional modifier keys
202
+ */
203
+ pressKey(key: string, modifiers?: TuiKeyModifiers): Promise<void>;
204
+ /**
205
+ * Send raw text without any interpretation.
206
+ * Useful for pasting content or sending special sequences.
207
+ * @param text - Raw text to send
208
+ */
209
+ sendText(text: string): Promise<void>;
210
+ /**
211
+ * Fill a labeled field in the TUI.
212
+ * Navigates to the field and enters the value.
213
+ * Similar to UiPort.fillLabel.
214
+ * @param fieldLabel - The label or identifier of the field
215
+ * @param value - The value to enter
216
+ */
217
+ fillField(fieldLabel: string, value: string): Promise<void>;
218
+ /**
219
+ * Select an option/menu item.
220
+ * Similar to UiPort.clickButton conceptually.
221
+ * @param option - The option text to select
222
+ */
223
+ selectOption(option: string): Promise<void>;
224
+ /**
225
+ * Send a mouse event to the terminal.
226
+ * Only works if the TUI application supports mouse input.
227
+ * @param event - Mouse event configuration
228
+ */
229
+ sendMouse(event: TuiMouseEvent): Promise<void>;
230
+ /**
231
+ * Click at a specific position.
232
+ * @param x - Column position (0-based)
233
+ * @param y - Row position (0-based)
234
+ * @param button - Mouse button (defaults to 'left')
235
+ */
236
+ click(x: number, y: number, button?: TuiMouseButton): Promise<void>;
237
+ /**
238
+ * Click on text content in the terminal.
239
+ * Finds the text and clicks on its position.
240
+ * @param text - The text to find and click
241
+ */
242
+ clickOnText(text: string): Promise<void>;
243
+ /**
244
+ * Assert that text is visible on the screen.
245
+ * Waits for the text to appear within timeout.
246
+ * Similar to UiPort.expectText.
247
+ * @param text - The expected text
248
+ * @param options - Wait options
249
+ */
250
+ expectText(text: string, options?: TuiWaitOptions): Promise<void>;
251
+ /**
252
+ * Assert that text matching a pattern is visible.
253
+ * @param pattern - Regular expression to match
254
+ * @param options - Wait options
255
+ */
256
+ expectPattern(pattern: RegExp, options?: TuiWaitOptions): Promise<void>;
257
+ /**
258
+ * Assert that text is NOT visible on the screen.
259
+ * @param text - The text that should not be present
260
+ */
261
+ expectNotText(text: string): Promise<void>;
262
+ /**
263
+ * Assert the screen contains specific text (immediate check, no wait).
264
+ * @param text - The expected text
265
+ */
266
+ assertScreenContains(text: string): Promise<void>;
267
+ /**
268
+ * Assert the screen matches a regular expression.
269
+ * @param pattern - Regular expression to match against screen content
270
+ */
271
+ assertScreenMatches(pattern: RegExp): Promise<void>;
272
+ /**
273
+ * Wait for text to appear on screen.
274
+ * @param text - Text to wait for
275
+ * @param options - Wait options
276
+ */
277
+ waitForText(text: string, options?: TuiWaitOptions): Promise<void>;
278
+ /**
279
+ * Wait for text matching pattern to appear.
280
+ * @param pattern - Regular expression to match
281
+ * @param options - Wait options
282
+ */
283
+ waitForPattern(pattern: RegExp, options?: TuiWaitOptions): Promise<void>;
284
+ /**
285
+ * Wait for the application to be ready.
286
+ * Implementation-specific (e.g., wait for prompt, initial screen).
287
+ */
288
+ waitForReady(): Promise<void>;
289
+ /**
290
+ * Wait for a specific duration.
291
+ * Similar to UiPort.waitSeconds.
292
+ * @param seconds - Duration to wait in seconds
293
+ */
294
+ waitSeconds(seconds: number): Promise<void>;
295
+ /**
296
+ * Capture the current screen state.
297
+ * @returns Screen capture with text content and metadata
298
+ */
299
+ captureScreen(): Promise<TuiScreenCapture>;
300
+ /**
301
+ * Get the current screen text content.
302
+ * @returns Plain text representation of the screen
303
+ */
304
+ getScreenText(): Promise<string>;
305
+ /**
306
+ * Get screen content as lines.
307
+ * @returns Array of screen lines
308
+ */
309
+ getScreenLines(): Promise<string[]>;
310
+ /**
311
+ * Take a snapshot of the current screen state.
312
+ * Saves to the configured snapshot directory.
313
+ * @param name - Snapshot identifier
314
+ */
315
+ takeSnapshot(name: string): Promise<void>;
316
+ /**
317
+ * Compare current screen against a saved snapshot.
318
+ * @param name - Snapshot identifier to compare against
319
+ * @returns Comparison result with pass/fail and diff
320
+ */
321
+ matchSnapshot(name: string): Promise<TuiSnapshotResult>;
322
+ /**
323
+ * Clear the terminal screen.
324
+ */
325
+ clear(): Promise<void>;
326
+ /**
327
+ * Resize the terminal.
328
+ * @param size - New dimensions
329
+ */
330
+ resize(size: {
331
+ cols: number;
332
+ rows: number;
333
+ }): Promise<void>;
334
+ /**
335
+ * Get the current terminal size.
336
+ */
337
+ getSize(): {
338
+ cols: number;
339
+ rows: number;
340
+ };
341
+ /**
342
+ * Get the current configuration.
343
+ */
344
+ getConfig(): TuiConfig;
345
+ }
346
+
347
+ type CreateContext = PlaywrightTestArgs & PlaywrightWorkerArgs & {
348
+ apiRequest: APIRequestContext;
349
+ page: Page;
350
+ };
351
+ /**
352
+ * Factory function type for creating a TUI adapter.
353
+ * Returns undefined if TUI testing is not configured.
354
+ */
355
+ type TuiFactory = () => TuiPort | undefined;
356
+ type CreateBddTestOptions = {
357
+ createApi?: (ctx: CreateContext) => ApiPort;
358
+ createUi?: (ctx: CreateContext) => UiPort;
359
+ createAuth?: (ctx: CreateContext & {
360
+ api: ApiPort;
361
+ ui: UiPort;
362
+ }) => AuthPort;
363
+ createCleanup?: (ctx: CreateContext) => CleanupPort;
364
+ /**
365
+ * Factory function for creating a TUI adapter.
366
+ * Unlike other adapters, this is a simple factory that doesn't receive context,
367
+ * as TUI testing operates independently of Playwright's browser context.
368
+ *
369
+ * @example
370
+ * ```typescript
371
+ * createTui: () => new TuiTesterAdapter({
372
+ * command: ['node', 'dist/cli.js'],
373
+ * size: { cols: 100, rows: 30 },
374
+ * }),
375
+ * ```
376
+ */
377
+ createTui?: TuiFactory;
378
+ worldFactory?: () => World;
379
+ };
380
+ declare function createBddTest(options?: CreateBddTestOptions): _playwright_test.TestType<PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & playwright_bdd.BddTestFixtures & {
381
+ world: World;
382
+ api: ApiPort;
383
+ ui: UiPort;
384
+ auth: AuthPort;
385
+ cleanup: CleanupPort;
386
+ tui: TuiPort | undefined;
387
+ apiRequest: APIRequestContext;
388
+ }, PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions & playwright_bdd.BddWorkerFixtures>;
389
+
390
+ declare function interpolate(template: string, vars: Record<string, string>): string;
391
+ declare function tryParseJson(text: string): unknown;
392
+ declare function selectPath(root: unknown, path: string): unknown;
393
+ declare function parseExpected(input: string, world: World): unknown;
394
+ declare function assertMasked(val: unknown): void;
395
+ declare function registerCleanup(world: World, item: {
396
+ method?: 'DELETE' | 'POST' | 'PATCH' | 'PUT';
397
+ path: string;
398
+ }): void;
399
+
400
+ declare class PlaywrightApiAdapter implements ApiPort {
401
+ private readonly request;
402
+ constructor(request: APIRequestContext);
403
+ sendJson(method: ApiMethod, path: string, body?: unknown, headers?: Record<string, string>): Promise<ApiResult>;
404
+ sendForm(method: 'POST' | 'PUT' | 'PATCH', path: string, form: Record<string, string>, headers?: Record<string, string>): Promise<ApiResult>;
405
+ }
406
+
407
+ declare class PlaywrightUiAdapter implements UiPort {
408
+ private readonly page;
409
+ constructor(page: Page);
410
+ goto(path: string): Promise<void>;
411
+ clickButton(name: string): Promise<void>;
412
+ clickLink(name: string): Promise<void>;
413
+ fillPlaceholder(placeholder: string, value: string): Promise<void>;
414
+ fillLabel(label: string, value: string): Promise<void>;
415
+ expectText(text: string): Promise<void>;
416
+ expectUrlContains(part: string): Promise<void>;
417
+ goBack(): Promise<void>;
418
+ reload(): Promise<void>;
419
+ waitSeconds(seconds: number): Promise<void>;
420
+ waitForPageLoad(): Promise<void>;
421
+ getCurrentUrl(): Promise<string>;
422
+ zoomTo(scale: number): Promise<void>;
423
+ typeText(text: string): Promise<void>;
424
+ pressKey(key: string): Promise<void>;
425
+ clickElementThatContains(clickMode: UiClickMode, elementType: string, text: string): Promise<void>;
426
+ clickElementWith(clickMode: UiClickMode, ordinal: string, text: string, method: UiLocatorMethod): Promise<void>;
427
+ fillDropdown(value: string, dropdownLabel: string): Promise<void>;
428
+ inputInElement(action: UiInputMode, value: string, ordinal: string, text: string, method: UiLocatorMethod): Promise<void>;
429
+ expectUrl(mode: UiUrlAssertMode, expected: string): Promise<void>;
430
+ expectNewTabUrl(mode: UiUrlAssertMode, expected: string): Promise<void>;
431
+ expectElementWithTextVisible(elementType: string, text: string, shouldBeVisible: boolean): Promise<void>;
432
+ expectElementState(ordinal: string, text: string, method: UiLocatorMethod, state: UiElementState): Promise<void>;
433
+ expectElementStateWithin(ordinal: string, text: string, method: UiLocatorMethod, state: UiElementState, seconds: number): Promise<void>;
434
+ private parseOrdinal;
435
+ private locatorBy;
436
+ private performClick;
437
+ private expectState;
438
+ private assertUrlAgainst;
439
+ }
440
+
441
+ declare class UniversalAuthAdapter implements AuthPort {
442
+ private readonly deps;
443
+ constructor(deps: {
444
+ api: ApiPort;
445
+ ui: UiPort;
446
+ });
447
+ apiSetBearer(world: World, token: string): void;
448
+ apiLoginAsAdmin(world: World): Promise<void>;
449
+ apiLoginAsUser(world: World): Promise<void>;
450
+ private apiLogin;
451
+ uiLoginAsAdmin(world: World): Promise<void>;
452
+ uiLoginAsUser(world: World): Promise<void>;
453
+ private uiLogin;
454
+ }
455
+
456
+ type CleanupRule = {
457
+ varMatch: string;
458
+ method?: 'DELETE' | 'POST' | 'PATCH' | 'PUT';
459
+ path: string;
460
+ };
461
+ declare class DefaultCleanupAdapter implements CleanupPort {
462
+ private readonly rules;
463
+ private readonly allowHeuristic;
464
+ constructor(input?: {
465
+ rules?: CleanupRule[];
466
+ allowHeuristic?: boolean;
467
+ });
468
+ registerFromVar(world: World, varName: string, id: unknown, meta?: unknown): void;
469
+ }
470
+
471
+ /**
472
+ * TUI Tester Adapter
473
+ *
474
+ * Implements TuiPort using the tui-tester library (https://github.com/luxquant/tui-tester).
475
+ * Provides end-to-end testing capabilities for terminal user interfaces via tmux.
476
+ */
477
+
478
+ declare class TuiTesterAdapter implements TuiPort {
479
+ private tester;
480
+ private config;
481
+ private running;
482
+ private tuiTesterModule;
483
+ constructor(config: TuiConfig);
484
+ /**
485
+ * Lazily load the tui-tester module to support optional dependency
486
+ */
487
+ private loadTuiTester;
488
+ /**
489
+ * Get the tester instance, throwing if not started
490
+ */
491
+ private getTester;
492
+ start(): Promise<void>;
493
+ stop(): Promise<void>;
494
+ restart(): Promise<void>;
495
+ isRunning(): boolean;
496
+ typeText(text: string, options?: {
497
+ delay?: number;
498
+ }): Promise<void>;
499
+ pressKey(key: string, modifiers?: TuiKeyModifiers): Promise<void>;
500
+ sendText(text: string): Promise<void>;
501
+ fillField(fieldLabel: string, value: string): Promise<void>;
502
+ selectOption(option: string): Promise<void>;
503
+ sendMouse(event: TuiMouseEvent): Promise<void>;
504
+ click(x: number, y: number, button?: TuiMouseButton): Promise<void>;
505
+ clickOnText(text: string): Promise<void>;
506
+ expectText(text: string, options?: TuiWaitOptions): Promise<void>;
507
+ expectPattern(pattern: RegExp, options?: TuiWaitOptions): Promise<void>;
508
+ expectNotText(text: string): Promise<void>;
509
+ assertScreenContains(text: string): Promise<void>;
510
+ assertScreenMatches(pattern: RegExp): Promise<void>;
511
+ waitForText(text: string, options?: TuiWaitOptions): Promise<void>;
512
+ waitForPattern(pattern: RegExp, options?: TuiWaitOptions): Promise<void>;
513
+ waitForReady(): Promise<void>;
514
+ waitSeconds(seconds: number): Promise<void>;
515
+ captureScreen(): Promise<TuiScreenCapture>;
516
+ getScreenText(): Promise<string>;
517
+ getScreenLines(): Promise<string[]>;
518
+ takeSnapshot(name: string): Promise<void>;
519
+ matchSnapshot(name: string): Promise<TuiSnapshotResult>;
520
+ clear(): Promise<void>;
521
+ resize(size: {
522
+ cols: number;
523
+ rows: number;
524
+ }): Promise<void>;
525
+ getSize(): {
526
+ cols: number;
527
+ rows: number;
528
+ };
529
+ getConfig(): TuiConfig;
530
+ }
531
+
532
+ type TagsForProjectInput = {
533
+ projectTag: string;
534
+ extraTags?: string;
535
+ defaultExcludes?: string;
536
+ };
537
+ declare function tagsForProject({ projectTag, extraTags, defaultExcludes }: TagsForProjectInput): string;
538
+ declare function resolveExtraTags(raw?: string | null): string | undefined;
539
+
540
+ export { type ApiMethod, type ApiPort, type ApiResult, type AuthPort, type CleanupItem, type CleanupPort, type CleanupRule, type CreateBddTestOptions, DefaultCleanupAdapter, PlaywrightApiAdapter, PlaywrightUiAdapter, type TuiConfig, type TuiFactory, type TuiKeyModifiers, type TuiMouseButton, type TuiMouseEvent, type TuiMouseEventType, type TuiPort, type TuiScreenCapture, type TuiSnapshotResult, TuiTesterAdapter, type TuiWaitOptions, type UiClickMode, type UiElementState, type UiInputMode, type UiLocatorMethod, type UiPort, type UiUrlAssertMode, UniversalAuthAdapter, type World, assertMasked, createBddTest, initWorld, interpolate, parseExpected, registerCleanup, resolveExtraTags, selectPath, tagsForProject, tryParseJson };