create-appraisejs 0.1.9 → 0.1.10-alpha.1

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.
Files changed (159) hide show
  1. package/README.md +24 -17
  2. package/dist/cli.d.ts +2 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.e2e.test.js +11 -8
  5. package/dist/cli.e2e.test.js.map +1 -1
  6. package/dist/cli.js +32 -48
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +5 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/config.test.js +9 -5
  12. package/dist/config.test.js.map +1 -1
  13. package/dist/copy-template.d.ts +1 -1
  14. package/dist/copy-template.d.ts.map +1 -1
  15. package/dist/copy-template.js +7 -3
  16. package/dist/copy-template.js.map +1 -1
  17. package/dist/copy-template.test.js +14 -9
  18. package/dist/copy-template.test.js.map +1 -1
  19. package/dist/create-project.d.ts +23 -0
  20. package/dist/create-project.d.ts.map +1 -0
  21. package/dist/create-project.js +58 -0
  22. package/dist/create-project.js.map +1 -0
  23. package/dist/create-project.test.d.ts +2 -0
  24. package/dist/create-project.test.d.ts.map +1 -0
  25. package/dist/create-project.test.js +80 -0
  26. package/dist/create-project.test.js.map +1 -0
  27. package/dist/install.d.ts +8 -4
  28. package/dist/install.d.ts.map +1 -1
  29. package/dist/install.js +22 -72
  30. package/dist/install.js.map +1 -1
  31. package/dist/install.test.js +26 -10
  32. package/dist/install.test.js.map +1 -1
  33. package/dist/package-manager.d.ts +11 -0
  34. package/dist/package-manager.d.ts.map +1 -0
  35. package/dist/package-manager.js +47 -0
  36. package/dist/package-manager.js.map +1 -0
  37. package/dist/package-manager.test.d.ts +2 -0
  38. package/dist/package-manager.test.d.ts.map +1 -0
  39. package/dist/package-manager.test.js +51 -0
  40. package/dist/package-manager.test.js.map +1 -0
  41. package/dist/prepare-template-utils.d.ts +10 -0
  42. package/dist/prepare-template-utils.d.ts.map +1 -0
  43. package/dist/prepare-template-utils.js +53 -0
  44. package/dist/prepare-template-utils.js.map +1 -0
  45. package/dist/prepare-template-utils.test.d.ts +2 -0
  46. package/dist/prepare-template-utils.test.d.ts.map +1 -0
  47. package/dist/prepare-template-utils.test.js +67 -0
  48. package/dist/prepare-template-utils.test.js.map +1 -0
  49. package/dist/prompts.d.ts +2 -0
  50. package/dist/prompts.d.ts.map +1 -1
  51. package/dist/prompts.js +11 -3
  52. package/dist/prompts.js.map +1 -1
  53. package/dist/prompts.test.js +17 -7
  54. package/dist/prompts.test.js.map +1 -1
  55. package/dist/template-sync-utils.test.d.ts +2 -0
  56. package/dist/template-sync-utils.test.d.ts.map +1 -0
  57. package/dist/template-sync-utils.test.js +41 -0
  58. package/dist/template-sync-utils.test.js.map +1 -0
  59. package/package.json +3 -2
  60. package/templates/default/.appraise-template-meta.json +5 -0
  61. package/templates/default/.env.example +1 -1
  62. package/templates/default/.vscode/settings.json +10 -3
  63. package/templates/default/README.md +27 -25
  64. package/templates/default/automation/features/base/login.feature +15 -0
  65. package/templates/default/automation/locators/base/home.json +3 -0
  66. package/templates/default/automation/locators/base/login.json +6 -0
  67. package/templates/default/automation/locators/base/test.json +1 -0
  68. package/templates/default/automation/mapping/locator-map.json +14 -0
  69. package/templates/default/{src/tests → automation}/steps/actions/click.step.ts +1 -4
  70. package/templates/default/{src/tests → automation}/steps/actions/hover.step.ts +1 -4
  71. package/templates/default/{src/tests → automation}/steps/actions/input.step.ts +1 -4
  72. package/templates/default/{src/tests → automation}/steps/actions/navigation.step.ts +1 -3
  73. package/templates/default/{src/tests → automation}/steps/actions/random_data.step.ts +1 -3
  74. package/templates/default/{src/tests → automation}/steps/actions/store.step.ts +1 -4
  75. package/templates/default/automation/steps/actions/wait.step.ts +91 -0
  76. package/templates/default/{src/tests → automation}/steps/validations/active_state_assertion.step.ts +1 -4
  77. package/templates/default/{src/tests → automation}/steps/validations/navigation_assertion.step.ts +1 -2
  78. package/templates/default/{src/tests → automation}/steps/validations/text_assertion.step.ts +1 -4
  79. package/templates/default/{src/tests → automation}/steps/validations/visibility_assertion.step.ts +1 -4
  80. package/templates/default/cucumber.mjs +6 -6
  81. package/templates/default/eslint.config.mjs +5 -4
  82. package/templates/default/package-lock.json +322 -485
  83. package/templates/default/package.json +12 -6
  84. package/templates/default/packages/cucumber-runtime/package.json +13 -0
  85. package/templates/default/packages/cucumber-runtime/src/cache.util.ts +93 -0
  86. package/templates/default/packages/cucumber-runtime/src/cli.ts +68 -0
  87. package/templates/default/packages/cucumber-runtime/src/environment.util.ts +21 -0
  88. package/templates/default/packages/cucumber-runtime/src/executor.ts +32 -0
  89. package/templates/default/{src/tests/hooks → packages/cucumber-runtime/src}/hooks.ts +17 -32
  90. package/templates/default/packages/cucumber-runtime/src/index.ts +17 -0
  91. package/templates/default/{src/tests/utils → packages/cucumber-runtime/src}/locator.util.ts +50 -64
  92. package/templates/default/packages/cucumber-runtime/src/parameter-types.ts +7 -0
  93. package/templates/default/packages/cucumber-runtime/src/paths.ts +33 -0
  94. package/templates/default/packages/cucumber-runtime/src/random-data.util.ts +35 -0
  95. package/templates/default/packages/cucumber-runtime/src/types.ts +13 -0
  96. package/templates/default/{src/tests/config/executor → packages/cucumber-runtime/src}/world.ts +4 -1
  97. package/templates/default/packages/cucumber-runtime/tsconfig.json +11 -0
  98. package/templates/default/scripts/setup-env.ts +4 -4
  99. package/templates/default/scripts/sync-appraise-base-template.ts +124 -105
  100. package/templates/default/scripts/sync-environments.ts +8 -5
  101. package/templates/default/scripts/sync-locator-groups.ts +7 -10
  102. package/templates/default/scripts/sync-locators.ts +5 -9
  103. package/templates/default/scripts/sync-modules.ts +9 -17
  104. package/templates/default/scripts/sync-tags.ts +2 -2
  105. package/templates/default/scripts/sync-template-step-groups.ts +16 -6
  106. package/templates/default/scripts/sync-template-steps.ts +16 -5
  107. package/templates/default/scripts/sync-test-cases.ts +6 -3
  108. package/templates/default/scripts/sync-test-suites.ts +7 -4
  109. package/templates/default/src/actions/environments/environment-actions.ts +6 -23
  110. package/templates/default/src/actions/locator/locator-actions.ts +36 -93
  111. package/templates/default/src/actions/locator-groups/locator-group-actions.ts +24 -78
  112. package/templates/default/src/actions/modules/module-actions.ts +4 -2
  113. package/templates/default/src/actions/tags/tag-actions.ts +4 -1
  114. package/templates/default/src/actions/template-step/template-step-actions.ts +10 -101
  115. package/templates/default/src/actions/template-step-group/template-step-group-actions.ts +31 -130
  116. package/templates/default/src/actions/test-case/test-case-actions.ts +31 -94
  117. package/templates/default/src/actions/test-run/test-run-actions.ts +11 -13
  118. package/templates/default/src/actions/test-suite/test-suite-actions.ts +29 -82
  119. package/templates/default/src/app/(base)/locator-groups/page.tsx +1 -3
  120. package/templates/default/src/app/(base)/reports/page.tsx +1 -1
  121. package/templates/default/src/app/(base)/reports/test-cases/page.tsx +2 -2
  122. package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table-columns.tsx +1 -1
  123. package/templates/default/src/app/(base)/tags/page.tsx +2 -2
  124. package/templates/default/src/app/(base)/template-steps/page.tsx +1 -2
  125. package/templates/default/src/app/(base)/test-runs/page.tsx +2 -2
  126. package/templates/default/src/app/api/test-runs/[runId]/logs/route.ts +2 -1
  127. package/templates/default/src/app/api/test-runs/[runId]/trace/[testCaseId]/route.ts +2 -1
  128. package/templates/default/src/app/page.tsx +4 -5
  129. package/templates/default/src/components/diagram/dynamic-parameters.tsx +76 -40
  130. package/templates/default/src/components/diagram/options-header-node.tsx +1 -1
  131. package/templates/default/src/components/ui/data-table.tsx +33 -39
  132. package/templates/default/src/lib/automation/paths.ts +181 -0
  133. package/templates/default/src/lib/automation/projection-service.ts +230 -0
  134. package/templates/default/src/lib/environment-file-utils.ts +14 -51
  135. package/templates/default/src/lib/executor/local-executor-adapter.ts +101 -0
  136. package/templates/default/src/lib/executor/types.ts +24 -0
  137. package/templates/default/src/lib/feature-file-generator.ts +22 -112
  138. package/templates/default/src/lib/locator-group-file-utils.ts +57 -120
  139. package/templates/default/src/lib/process/task-spawner.ts +236 -0
  140. package/templates/default/src/lib/template-sync-utils.d.ts +7 -0
  141. package/templates/default/src/lib/template-sync-utils.d.ts.map +1 -0
  142. package/templates/default/src/lib/template-sync-utils.js +47 -0
  143. package/templates/default/src/lib/template-sync-utils.js.map +1 -0
  144. package/templates/default/src/lib/template-sync-utils.ts +63 -0
  145. package/templates/default/src/lib/test-run/process-manager.ts +9 -87
  146. package/templates/default/src/lib/test-run/test-run-executor.ts +7 -136
  147. package/templates/default/src/lib/test-run/winston-logger.ts +6 -35
  148. package/templates/default/src/lib/utils/template-step-file-generator.ts +22 -85
  149. package/templates/default/src/lib/utils/template-step-file-manager-intelligent.ts +7 -22
  150. package/templates/default/public/favicon.ico +0 -0
  151. package/templates/default/src/tests/executor.ts +0 -80
  152. package/templates/default/src/tests/mapping/locator-map.json +0 -1
  153. package/templates/default/src/tests/steps/actions/wait.step.ts +0 -107
  154. package/templates/default/src/tests/support/parameter-types.ts +0 -12
  155. package/templates/default/src/tests/utils/cache.util.ts +0 -260
  156. package/templates/default/src/tests/utils/cli.util.ts +0 -177
  157. package/templates/default/src/tests/utils/environment.util.ts +0 -65
  158. package/templates/default/src/tests/utils/random-data.util.ts +0 -45
  159. package/templates/default/src/tests/utils/spawner.util.ts +0 -617
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "name": "appraise",
3
- "version": "0.1.0",
3
+ "version": "0.1.9-alpha",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "next dev",
8
8
  "init": "DISABLE_DEVTOOLS=1 NEXT_PUBLIC_DISABLE_DEVTOOLS=1 next dev",
9
- "build": "next build",
9
+ "build:cucumber-runtime": "tsc -p packages/cucumber-runtime/tsconfig.json",
10
+ "build": "npm run build:local",
10
11
  "start": "next start",
11
12
  "lint": "eslint .",
12
- "install-playwright": "npx playwright install && npx playwright install-deps",
13
+ "install-playwright": "npx playwright install",
13
14
  "install-dependencies": "npm install --legacy-peer-deps",
14
15
  "setup-env": "npx tsx scripts/setup-env.ts",
15
- "migrate-db": "npx prisma migrate dev",
16
- "setup": "npm run install-dependencies && npm run setup-env && npm run migrate-db && npm run install-playwright && npm run sync-all",
16
+ "migrate-db": "npx prisma migrate deploy",
17
+ "setup": "npm run install-dependencies && npm run setup:db && npm run build:local",
17
18
  "sync-features": "npx tsx scripts/regenerate-features.ts",
18
19
  "sync-features:dry-run": "npx tsx scripts/regenerate-features.ts --dry-run",
19
20
  "sync-locator-groups": "npx tsx scripts/sync-locator-groups.ts",
@@ -28,6 +29,10 @@
28
29
  "sync-all": "npx tsx scripts/sync-all.ts",
29
30
  "sync-template": "npx tsx scripts/sync-appraise-base-template.ts",
30
31
  "test": "cucumber-js",
32
+ "build:local": "npm run generate-db-client && npm run build:cucumber-runtime && next build",
33
+ "generate-db-client": "npx prisma generate --schema prisma/schema.prisma",
34
+ "setup:db": "npm run setup-env && npm run generate-db-client && npm run migrate-db && npm run sync-all",
35
+ "setup:full": "npm run install-dependencies && npm run setup:db && npm run build:local",
31
36
  "appraisejs:setup": "npm run setup",
32
37
  "appraisejs:sync": "npm run sync-all"
33
38
  },
@@ -121,6 +126,7 @@
121
126
  "overrides": {
122
127
  "react-is": "^19.2.1",
123
128
  "@types/react": "^19.2.7",
124
- "@types/react-dom": "^19.2.3"
129
+ "@types/react-dom": "^19.2.3",
130
+ "minimatch": ">=10.2.1"
125
131
  }
126
132
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@appraise/cucumber-runtime",
3
+ "private": true,
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.json"
12
+ }
13
+ }
@@ -0,0 +1,93 @@
1
+ import { readFileSync } from 'fs'
2
+ import { globSync } from 'glob'
3
+ import * as path from 'path'
4
+ import { LocatorCollection, LocatorMap } from './types.js'
5
+ import { getAutomationLocatorMapPath, getAutomationLocatorsDir } from './paths.js'
6
+
7
+ function toGlobPath(targetPath: string): string {
8
+ return targetPath.split(path.sep).join('/')
9
+ }
10
+
11
+ export class LocatorCache {
12
+ private static instance: LocatorCache
13
+ private data: Record<string, LocatorCollection> = {}
14
+ private filePaths: Record<string, string> = {}
15
+ private loadedFiles: Set<string> = new Set()
16
+
17
+ private constructor() {
18
+ globSync(`${toGlobPath(getAutomationLocatorsDir())}/**/*.json`).forEach(file => {
19
+ const fileName = path.basename(file, path.extname(file))
20
+ this.filePaths[fileName ?? file] = file
21
+ })
22
+ }
23
+
24
+ public static getInstance(): LocatorCache {
25
+ if (!LocatorCache.instance) {
26
+ LocatorCache.instance = new LocatorCache()
27
+ }
28
+ return LocatorCache.instance
29
+ }
30
+
31
+ public get(key: string): LocatorCollection | null {
32
+ if (!this.loadedFiles.has(key) && this.filePaths[key]) {
33
+ try {
34
+ const filePath = this.filePaths[key]
35
+ const data = JSON.parse(readFileSync(filePath, 'utf8'))
36
+ this.data[key] = data as LocatorCollection
37
+ this.loadedFiles.add(key)
38
+ } catch (error) {
39
+ console.error(`Error loading locator file for key "${key}":`, error)
40
+ return null
41
+ }
42
+ }
43
+
44
+ return this.data[key] ? { ...this.data[key] } : null
45
+ }
46
+
47
+ public preloadAll(): void {
48
+ Object.keys(this.filePaths).forEach(key => {
49
+ if (!this.loadedFiles.has(key)) {
50
+ this.get(key)
51
+ }
52
+ })
53
+ }
54
+
55
+ public getAvailableKeys(): string[] {
56
+ return Object.keys(this.filePaths)
57
+ }
58
+
59
+ public isLoaded(key: string): boolean {
60
+ return this.loadedFiles.has(key)
61
+ }
62
+ }
63
+
64
+ export class LocatorMapCache {
65
+ private static instance: LocatorMapCache
66
+ private data: LocatorMap[] = []
67
+
68
+ private constructor() {
69
+ this.data = JSON.parse(readFileSync(getAutomationLocatorMapPath(), 'utf8'))
70
+ }
71
+
72
+ public static getInstance(): LocatorMapCache {
73
+ if (!LocatorMapCache.instance) {
74
+ LocatorMapCache.instance = new LocatorMapCache()
75
+ }
76
+ return LocatorMapCache.instance
77
+ }
78
+
79
+ public get(key: string): LocatorMap {
80
+ try {
81
+ return this.data.find(map => map.path === key) as LocatorMap
82
+ } catch {
83
+ return {
84
+ name: '',
85
+ path: '',
86
+ }
87
+ }
88
+ }
89
+
90
+ public getAll(): LocatorMap[] {
91
+ return this.data
92
+ }
93
+ }
@@ -0,0 +1,68 @@
1
+ import { Command, Option, OptionValues } from 'commander'
2
+ import parseTagExpression from '@cucumber/tag-expressions'
3
+ import { getAllEnvironments } from './environment.util.js'
4
+ import { BrowserName } from './types.js'
5
+
6
+ export interface CliOptions extends OptionValues {
7
+ environment: string
8
+ tags: string
9
+ parallel: number
10
+ browser: BrowserName
11
+ headless: 'true' | 'false'
12
+ }
13
+
14
+ const BROWSER_CHOICES = ['chromium', 'firefox', 'webkit'] as const
15
+ const HEADLESS_CHOICES = ['true', 'false'] as const
16
+ const DEFAULT_PARALLEL_WORKERS = 1
17
+ const DEFAULT_BROWSER: BrowserName = 'chromium'
18
+ const DEFAULT_HEADLESS = 'true'
19
+
20
+ const program = new Command()
21
+
22
+ let environmentNames: string[] = []
23
+ try {
24
+ environmentNames = Object.keys(getAllEnvironments())
25
+ } catch (error) {
26
+ console.error('Failed to load environments:', error instanceof Error ? error.message : 'Unknown error')
27
+ process.exit(1)
28
+ }
29
+
30
+ function parsePositiveInt(value: string): number {
31
+ const parsed = Number(value)
32
+ if (!Number.isInteger(parsed) || parsed <= 0) {
33
+ throw new Error(`--parallel must be a positive integer, got "${value}"`)
34
+ }
35
+ return parsed
36
+ }
37
+
38
+ function validateCucumberTagExpression(value: string): string {
39
+ try {
40
+ parseTagExpression(value)
41
+ return value
42
+ } catch (error) {
43
+ throw new Error(`Invalid tag expression: ${error instanceof Error ? error.message : 'Unknown error'}`)
44
+ }
45
+ }
46
+
47
+ program
48
+ .name('cucumber-runtime')
49
+ .description('CLI for running Appraise cucumber automation')
50
+ .version('1.0.0')
51
+ .addOption(
52
+ new Option('-e, --environment <environment>', 'The environment to run the tests on')
53
+ .choices(environmentNames)
54
+ .makeOptionMandatory(),
55
+ )
56
+ .addOption(
57
+ new Option('-t, --tags <tags>', 'The tags to run the tests on')
58
+ .argParser(validateCucumberTagExpression)
59
+ .makeOptionMandatory(),
60
+ )
61
+ .option('-p, --parallel <parallel>', 'The number of parallel workers to run', parsePositiveInt, DEFAULT_PARALLEL_WORKERS)
62
+ .addOption(new Option('-b, --browser <browser>', 'The browser to run').choices(BROWSER_CHOICES).default(DEFAULT_BROWSER))
63
+ .addOption(new Option('--headless <headless>', 'Whether to run headless').choices(HEADLESS_CHOICES).default(DEFAULT_HEADLESS))
64
+
65
+ export function startCli(): CliOptions {
66
+ program.parse()
67
+ return program.opts() as CliOptions
68
+ }
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from 'fs'
2
+ import { getAutomationEnvironmentsFilePath } from './paths.js'
3
+
4
+ interface EnvironmentConfig {
5
+ baseUrl: string
6
+ apiBaseUrl: string
7
+ email: string
8
+ password: string
9
+ }
10
+
11
+ export function getEnvironment(environment: string): EnvironmentConfig {
12
+ const environmentConfig: Record<string, EnvironmentConfig> = JSON.parse(
13
+ readFileSync(getAutomationEnvironmentsFilePath(), 'utf8'),
14
+ )
15
+
16
+ return environmentConfig[environment.toLowerCase()] as EnvironmentConfig
17
+ }
18
+
19
+ export function getAllEnvironments(): Record<string, EnvironmentConfig> {
20
+ return JSON.parse(readFileSync(getAutomationEnvironmentsFilePath(), 'utf8'))
21
+ }
@@ -0,0 +1,32 @@
1
+ import { execa } from 'execa'
2
+ import { config } from 'dotenv'
3
+ import { startCli } from './cli.js'
4
+
5
+ async function bootstrap(): Promise<void> {
6
+ config()
7
+
8
+ try {
9
+ const { environment, tags, parallel, browser, headless } = startCli()
10
+
11
+ process.env.ENVIRONMENT = environment
12
+ process.env.HEADLESS = headless
13
+ process.env.BROWSER = browser
14
+
15
+ const cucumberArgs: string[] = ['cucumber-js', '-t', tags]
16
+ if (parallel > 1) {
17
+ cucumberArgs.push('--parallel', parallel.toString())
18
+ }
19
+
20
+ const subprocess = execa('npx', cucumberArgs, {
21
+ stdio: 'inherit',
22
+ })
23
+
24
+ const result = await subprocess
25
+ process.exit(result.exitCode ?? 0)
26
+ } catch (error) {
27
+ console.error(error instanceof Error ? error.message : 'Unknown error')
28
+ process.exit(1)
29
+ }
30
+ }
31
+
32
+ bootstrap()
@@ -1,36 +1,29 @@
1
1
  import { After, AfterAll, AfterStep, Before, BeforeAll, setDefaultTimeout } from '@cucumber/cucumber'
2
- import { CustomWorld } from '../config/executor/world.js'
3
- import { BrowserName } from '@/types/executor/browser.type'
4
2
  import { config } from 'dotenv'
5
- import { chromium, ChromiumBrowser, firefox, FirefoxBrowser, webkit, WebKitBrowser } from 'playwright'
3
+ import { promises as fs } from 'fs'
4
+ import { chromium, firefox, webkit, ChromiumBrowser, FirefoxBrowser, WebKitBrowser } from 'playwright'
5
+ import { getAutomationTraceDir } from './paths.js'
6
+ import { BrowserName } from './types.js'
7
+ import { CustomWorld } from './world.js'
6
8
 
7
- // Load environment variables
8
9
  config()
9
10
 
10
- // Initialize browser
11
11
  let browser: ChromiumBrowser | FirefoxBrowser | WebKitBrowser
12
-
13
- // Track scenario status
14
- let currentScenarioStatus: string = 'unknown'
12
+ let currentScenarioStatus = 'unknown'
15
13
 
16
14
  BeforeAll(async function () {
17
15
  setDefaultTimeout(60000)
18
16
  const browserName = (process.env.BROWSER as BrowserName) || 'chromium'
17
+
19
18
  switch (browserName) {
20
19
  case 'chromium':
21
- browser = await chromium.launch({
22
- headless: process.env.HEADLESS === 'true',
23
- })
20
+ browser = await chromium.launch({ headless: process.env.HEADLESS === 'true' })
24
21
  break
25
22
  case 'firefox':
26
- browser = await firefox.launch({
27
- headless: process.env.HEADLESS === 'true',
28
- })
23
+ browser = await firefox.launch({ headless: process.env.HEADLESS === 'true' })
29
24
  break
30
25
  case 'webkit':
31
- browser = await webkit.launch({
32
- headless: process.env.HEADLESS === 'true',
33
- })
26
+ browser = await webkit.launch({ headless: process.env.HEADLESS === 'true' })
34
27
  break
35
28
  default:
36
29
  throw new Error(`Invalid browser name: ${browserName}`)
@@ -49,7 +42,6 @@ Before(async function (this: CustomWorld) {
49
42
  })
50
43
 
51
44
  AfterStep(async function (this: CustomWorld, result) {
52
- // Track the worst status encountered (failed > skipped > passed)
53
45
  const status = result.result?.status
54
46
  if (status === 'FAILED') {
55
47
  currentScenarioStatus = 'failed'
@@ -61,33 +53,26 @@ AfterStep(async function (this: CustomWorld, result) {
61
53
  })
62
54
 
63
55
  After(async function (this: CustomWorld, scenario) {
64
- // Emit scenario end event as JSON to stdout
65
- // ProcessManager will parse this and re-emit it as an event
66
56
  let tracePath: string | undefined
67
57
  if (scenario.result?.status === 'FAILED') {
68
- tracePath = `${process.cwd()}/src/tests/reports/traces/${crypto.randomUUID()}.zip`
69
- await this.context.tracing.stop({
70
- path: tracePath,
71
- })
58
+ const traceDir = getAutomationTraceDir()
59
+ await fs.mkdir(traceDir, { recursive: true })
60
+ tracePath = `${traceDir}/${crypto.randomUUID()}.zip`
61
+ await this.context.tracing.stop({ path: tracePath })
72
62
  }
73
63
 
74
- const eventData = {
64
+ const eventJson = JSON.stringify({
75
65
  event: 'scenario::end',
76
66
  data: {
77
67
  scenarioName: scenario.pickle.name,
78
68
  status: currentScenarioStatus,
79
- tracePath: tracePath,
69
+ tracePath,
80
70
  },
81
- }
71
+ })
82
72
 
83
- // Write to stdout in a parseable JSON format
84
- // Use process.stdout.write to ensure it's captured by TaskSpawner
85
- const eventJson = JSON.stringify(eventData)
86
73
  console.log(eventJson)
87
- // Also write directly to stdout to ensure it's captured
88
74
  process.stdout.write(eventJson + '\n')
89
75
 
90
- // Reset status for next scenario
91
76
  currentScenarioStatus = 'unknown'
92
77
 
93
78
  await this.page.close()
@@ -0,0 +1,17 @@
1
+ export {
2
+ After,
3
+ AfterAll,
4
+ AfterStep,
5
+ Before,
6
+ BeforeAll,
7
+ Given,
8
+ Then,
9
+ When,
10
+ defineParameterType,
11
+ setDefaultTimeout,
12
+ } from '@cucumber/cucumber'
13
+ export { CustomWorld, expect } from './world.js'
14
+ export { getEnvironment, getAllEnvironments } from './environment.util.js'
15
+ export { resolveLocator, retry, waitForRouteSettled } from './locator.util.js'
16
+ export { generateRandomData, RandomDataType } from './random-data.util.js'
17
+ export type { BrowserName, Locator, LocatorCollection, LocatorMap, Selector, SelectorName } from './types.js'
@@ -1,11 +1,8 @@
1
1
  import { Page } from 'playwright'
2
2
  import { LocatorCache, LocatorMapCache } from './cache.util.js'
3
3
 
4
- const sleep = (ms: number) => new Promise(r => setTimeout(r, ms))
4
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
5
5
 
6
- /**
7
- * Retry helper: immediate first attempt, exponential backoff, selective retry.
8
- */
9
6
  export async function retry<T>(
10
7
  fn: () => Promise<T>,
11
8
  {
@@ -17,7 +14,7 @@ export async function retry<T>(
17
14
  retries?: number
18
15
  delayMs?: number
19
16
  backoff?: number
20
- shouldRetry?: (e: unknown) => boolean
17
+ shouldRetry?: (error: unknown) => boolean
21
18
  } = {},
22
19
  ): Promise<T> {
23
20
  let lastErr: unknown
@@ -26,9 +23,11 @@ export async function retry<T>(
26
23
  for (let i = 0; i <= retries; i++) {
27
24
  try {
28
25
  return await fn()
29
- } catch (e) {
30
- lastErr = e
31
- if (i === retries || !shouldRetry(e)) break
26
+ } catch (error) {
27
+ lastErr = error
28
+ if (i === retries || !shouldRetry(error)) {
29
+ break
30
+ }
32
31
  await sleep(delay)
33
32
  delay = Math.floor(delay * backoff)
34
33
  }
@@ -38,20 +37,14 @@ export async function retry<T>(
38
37
  }
39
38
 
40
39
  const routeKey = (page: Page, includeSearch = false) => {
41
- const u = new URL(page.url())
42
- return includeSearch ? `${u.pathname}${u.search}` : u.pathname
40
+ const url = new URL(page.url())
41
+ return includeSearch ? `${url.pathname}${url.search}` : url.pathname
43
42
  }
44
43
 
45
- /**
46
- * Wait for route to be "settled" without AUT changes:
47
- * - URL stable for urlStableMs
48
- * - no frame navigations during that window
49
- * - DOM quiet (no mutations) for domQuietMs
50
- */
51
44
  export async function waitForRouteSettled(
52
45
  page: Page,
53
46
  {
54
- timeoutMs = 15_000,
47
+ timeoutMs = 15000,
55
48
  pollMs = 50,
56
49
  urlStableMs = 500,
57
50
  domQuietMs = 300,
@@ -65,16 +58,16 @@ export async function waitForRouteSettled(
65
58
  } = {},
66
59
  ): Promise<string> {
67
60
  const deadline = Date.now() + timeoutMs
68
-
69
61
  await page.waitForLoadState('domcontentloaded', { timeout: timeoutMs })
70
62
 
71
63
  let lastKey = routeKey(page, includeSearch)
72
64
  let stableSince = Date.now()
73
-
74
65
  let navBumpedAt = Date.now()
66
+
75
67
  const onFrameNav = () => {
76
68
  navBumpedAt = Date.now()
77
69
  }
70
+
78
71
  page.on('framenavigated', onFrameNav)
79
72
 
80
73
  try {
@@ -91,31 +84,36 @@ export async function waitForRouteSettled(
91
84
  const now = Date.now()
92
85
  const urlStableLongEnough = now - stableSince >= urlStableMs
93
86
  const noRecentNav = now - navBumpedAt >= urlStableMs
94
- if (!urlStableLongEnough || !noRecentNav) continue
87
+ if (!urlStableLongEnough || !noRecentNav) {
88
+ continue
89
+ }
95
90
 
96
91
  const domQuiet = await page
97
92
  .evaluate(quietMs => {
98
93
  return new Promise<boolean>(resolve => {
99
94
  let lastMutation = Date.now()
100
- const obs = new MutationObserver(() => {
95
+ const observer = new MutationObserver(() => {
101
96
  lastMutation = Date.now()
102
97
  })
103
- obs.observe(document, { subtree: true, childList: true, attributes: true })
98
+ observer.observe(document, { subtree: true, childList: true, attributes: true })
104
99
 
105
100
  const tick = () => {
106
101
  if (Date.now() - lastMutation >= quietMs) {
107
- obs.disconnect()
102
+ observer.disconnect()
108
103
  resolve(true)
109
104
  return
110
105
  }
111
106
  setTimeout(tick, 50)
112
107
  }
108
+
113
109
  tick()
114
110
  })
115
111
  }, domQuietMs)
116
112
  .catch(() => false)
117
113
 
118
- if (domQuiet) return lastKey
114
+ if (domQuiet) {
115
+ return lastKey
116
+ }
119
117
  }
120
118
 
121
119
  return routeKey(page, includeSearch)
@@ -124,24 +122,18 @@ export async function waitForRouteSettled(
124
122
  }
125
123
  }
126
124
 
127
- /**
128
- * Throw tagged error so we only retry on "map missing" cases.
129
- */
130
125
  const getLocatorMapData = async (page: Page) => {
131
126
  const locatorMap = LocatorMapCache.getInstance()
132
127
  const currentPath = new URL(page.url()).pathname
133
-
134
128
  const data = locatorMap.get(currentPath)
135
- if (!data) throw new Error(`LOCATOR_MAP_NOT_FOUND::${currentPath}`)
129
+
130
+ if (!data) {
131
+ throw new Error(`LOCATOR_MAP_NOT_FOUND::${currentPath}`)
132
+ }
133
+
136
134
  return data
137
135
  }
138
136
 
139
- /**
140
- * Validate that a selector "belongs" to the current page state.
141
- * - attached: element exists in DOM
142
- * - visible: element is visible (optional but very useful to avoid stale hidden elements)
143
- * - unique: avoids matching some leftover layout/header element found everywhere
144
- */
145
137
  async function validateResolvedSelector(
146
138
  page: Page,
147
139
  selector: string,
@@ -156,19 +148,18 @@ async function validateResolvedSelector(
156
148
  } = {},
157
149
  ): Promise<boolean> {
158
150
  try {
159
- const loc = page.locator(selector)
160
-
161
- // "attached" (exists) quickly
162
- await loc.first().waitFor({ state: 'attached', timeout: timeoutMs })
151
+ const locator = page.locator(selector)
152
+ await locator.first().waitFor({ state: 'attached', timeout: timeoutMs })
163
153
 
164
154
  if (requireVisible) {
165
- // visible check helps avoid stale hidden DOM (old route kept around)
166
- await loc.first().waitFor({ state: 'visible', timeout: timeoutMs })
155
+ await locator.first().waitFor({ state: 'visible', timeout: timeoutMs })
167
156
  }
168
157
 
169
158
  if (requireUnique) {
170
- const c = await loc.count()
171
- if (c !== 1) return false
159
+ const count = await locator.count()
160
+ if (count !== 1) {
161
+ return false
162
+ }
172
163
  }
173
164
 
174
165
  return true
@@ -177,13 +168,6 @@ async function validateResolvedSelector(
177
168
  }
178
169
  }
179
170
 
180
- /**
181
- * Resolve locator with:
182
- * - route settled
183
- * - map lookup retry
184
- * - selector validation (protects against stale "same key across pages")
185
- * - rerun if validation fails or route changes mid-flight
186
- */
187
171
  export async function resolveLocator(
188
172
  page: Page,
189
173
  locatorName: string,
@@ -202,11 +186,11 @@ export async function resolveLocator(
202
186
  requireUnique?: boolean
203
187
  }
204
188
  } = {},
205
- ) {
189
+ ): Promise<string | null> {
206
190
  try {
207
191
  for (let pass = 0; pass < maxResolvePasses; pass++) {
208
192
  await waitForRouteSettled(page, {
209
- timeoutMs: 15_000,
193
+ timeoutMs: 15000,
210
194
  urlStableMs: 500,
211
195
  domQuietMs: 300,
212
196
  pollMs: 50,
@@ -214,30 +198,32 @@ export async function resolveLocator(
214
198
  })
215
199
 
216
200
  const beforePath = new URL(page.url()).pathname
217
-
218
201
  const locatorMapData = await retry(() => getLocatorMapData(page), {
219
202
  retries: 6,
220
203
  delayMs: 80,
221
204
  backoff: 1.5,
222
- shouldRetry: e => e instanceof Error && e.message.startsWith('LOCATOR_MAP_NOT_FOUND::'),
205
+ shouldRetry: error => error instanceof Error && error.message.startsWith('LOCATOR_MAP_NOT_FOUND::'),
223
206
  })
224
207
 
225
208
  const locators = LocatorCache.getInstance().get(locatorMapData.name)
226
- if (!locators) throw new Error(`Locator bundle not found for name ${locatorMapData.name}`)
209
+ if (!locators) {
210
+ throw new Error(`Locator bundle not found for name ${locatorMapData.name}`)
211
+ }
227
212
 
228
213
  const selector = locators[locatorName]
229
- if (!selector) throw new Error(`Locator "${locatorName}" not found for name ${locatorMapData.name}`)
214
+ if (!selector) {
215
+ throw new Error(`Locator "${locatorName}" not found for name ${locatorMapData.name}`)
216
+ }
230
217
 
231
- // Guard: route changed mid-resolution → re-run
232
218
  const afterPath = new URL(page.url()).pathname
233
- if (afterPath !== beforePath) continue
234
-
235
- // Critical: validate selector matches current page state.
236
- const ok = await validateResolvedSelector(page, selector as unknown as string, validate)
237
- if (ok) return selector as unknown as string
219
+ if (afterPath !== beforePath) {
220
+ continue
221
+ }
238
222
 
239
- // If validation failed, likely stale bundle (same locator key on multiple pages),
240
- // or transition still in progress. Loop and re-resolve after settling again.
223
+ const isValid = await validateResolvedSelector(page, selector as unknown as string, validate)
224
+ if (isValid) {
225
+ return selector as unknown as string
226
+ }
241
227
  }
242
228
 
243
229
  throw new Error(`Failed to resolve a valid locator for "${locatorName}" after ${maxResolvePasses} passes`)
@@ -0,0 +1,7 @@
1
+ import { defineParameterType } from '@cucumber/cucumber'
2
+
3
+ defineParameterType({
4
+ name: 'boolean',
5
+ regexp: /true|false/,
6
+ transformer: (s: string) => s === 'true',
7
+ })
@@ -0,0 +1,33 @@
1
+ import path from 'path'
2
+
3
+ export function getAutomationRoot(): string {
4
+ return path.join(process.cwd(), 'automation')
5
+ }
6
+
7
+ export function getAutomationConfigDir(): string {
8
+ return path.join(getAutomationRoot(), 'config')
9
+ }
10
+
11
+ export function getAutomationEnvironmentsFilePath(): string {
12
+ return path.join(getAutomationConfigDir(), 'environments', 'environments.json')
13
+ }
14
+
15
+ export function getAutomationFeaturesDir(): string {
16
+ return path.join(getAutomationRoot(), 'features')
17
+ }
18
+
19
+ export function getAutomationLocatorsDir(): string {
20
+ return path.join(getAutomationRoot(), 'locators')
21
+ }
22
+
23
+ export function getAutomationLocatorMapPath(): string {
24
+ return path.join(getAutomationRoot(), 'mapping', 'locator-map.json')
25
+ }
26
+
27
+ export function getAutomationReportsDir(): string {
28
+ return path.join(getAutomationRoot(), 'reports')
29
+ }
30
+
31
+ export function getAutomationTraceDir(): string {
32
+ return path.join(getAutomationReportsDir(), 'traces')
33
+ }