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.
- package/README.md +24 -17
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.e2e.test.js +11 -8
- package/dist/cli.e2e.test.js.map +1 -1
- package/dist/cli.js +32 -48
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +9 -5
- package/dist/config.test.js.map +1 -1
- package/dist/copy-template.d.ts +1 -1
- package/dist/copy-template.d.ts.map +1 -1
- package/dist/copy-template.js +7 -3
- package/dist/copy-template.js.map +1 -1
- package/dist/copy-template.test.js +14 -9
- package/dist/copy-template.test.js.map +1 -1
- package/dist/create-project.d.ts +23 -0
- package/dist/create-project.d.ts.map +1 -0
- package/dist/create-project.js +58 -0
- package/dist/create-project.js.map +1 -0
- package/dist/create-project.test.d.ts +2 -0
- package/dist/create-project.test.d.ts.map +1 -0
- package/dist/create-project.test.js +80 -0
- package/dist/create-project.test.js.map +1 -0
- package/dist/install.d.ts +8 -4
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +22 -72
- package/dist/install.js.map +1 -1
- package/dist/install.test.js +26 -10
- package/dist/install.test.js.map +1 -1
- package/dist/package-manager.d.ts +11 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +47 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/package-manager.test.d.ts +2 -0
- package/dist/package-manager.test.d.ts.map +1 -0
- package/dist/package-manager.test.js +51 -0
- package/dist/package-manager.test.js.map +1 -0
- package/dist/prepare-template-utils.d.ts +10 -0
- package/dist/prepare-template-utils.d.ts.map +1 -0
- package/dist/prepare-template-utils.js +53 -0
- package/dist/prepare-template-utils.js.map +1 -0
- package/dist/prepare-template-utils.test.d.ts +2 -0
- package/dist/prepare-template-utils.test.d.ts.map +1 -0
- package/dist/prepare-template-utils.test.js +67 -0
- package/dist/prepare-template-utils.test.js.map +1 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +11 -3
- package/dist/prompts.js.map +1 -1
- package/dist/prompts.test.js +17 -7
- package/dist/prompts.test.js.map +1 -1
- package/dist/template-sync-utils.test.d.ts +2 -0
- package/dist/template-sync-utils.test.d.ts.map +1 -0
- package/dist/template-sync-utils.test.js +41 -0
- package/dist/template-sync-utils.test.js.map +1 -0
- package/package.json +3 -2
- package/templates/default/.appraise-template-meta.json +5 -0
- package/templates/default/.env.example +1 -1
- package/templates/default/.vscode/settings.json +10 -3
- package/templates/default/README.md +27 -25
- package/templates/default/automation/features/base/login.feature +15 -0
- package/templates/default/automation/locators/base/home.json +3 -0
- package/templates/default/automation/locators/base/login.json +6 -0
- package/templates/default/automation/locators/base/test.json +1 -0
- package/templates/default/automation/mapping/locator-map.json +14 -0
- package/templates/default/{src/tests → automation}/steps/actions/click.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/actions/hover.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/actions/input.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/actions/navigation.step.ts +1 -3
- package/templates/default/{src/tests → automation}/steps/actions/random_data.step.ts +1 -3
- package/templates/default/{src/tests → automation}/steps/actions/store.step.ts +1 -4
- package/templates/default/automation/steps/actions/wait.step.ts +91 -0
- package/templates/default/{src/tests → automation}/steps/validations/active_state_assertion.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/validations/navigation_assertion.step.ts +1 -2
- package/templates/default/{src/tests → automation}/steps/validations/text_assertion.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/validations/visibility_assertion.step.ts +1 -4
- package/templates/default/cucumber.mjs +6 -6
- package/templates/default/eslint.config.mjs +5 -4
- package/templates/default/package-lock.json +322 -485
- package/templates/default/package.json +12 -6
- package/templates/default/packages/cucumber-runtime/package.json +13 -0
- package/templates/default/packages/cucumber-runtime/src/cache.util.ts +93 -0
- package/templates/default/packages/cucumber-runtime/src/cli.ts +68 -0
- package/templates/default/packages/cucumber-runtime/src/environment.util.ts +21 -0
- package/templates/default/packages/cucumber-runtime/src/executor.ts +32 -0
- package/templates/default/{src/tests/hooks → packages/cucumber-runtime/src}/hooks.ts +17 -32
- package/templates/default/packages/cucumber-runtime/src/index.ts +17 -0
- package/templates/default/{src/tests/utils → packages/cucumber-runtime/src}/locator.util.ts +50 -64
- package/templates/default/packages/cucumber-runtime/src/parameter-types.ts +7 -0
- package/templates/default/packages/cucumber-runtime/src/paths.ts +33 -0
- package/templates/default/packages/cucumber-runtime/src/random-data.util.ts +35 -0
- package/templates/default/packages/cucumber-runtime/src/types.ts +13 -0
- package/templates/default/{src/tests/config/executor → packages/cucumber-runtime/src}/world.ts +4 -1
- package/templates/default/packages/cucumber-runtime/tsconfig.json +11 -0
- package/templates/default/scripts/setup-env.ts +4 -4
- package/templates/default/scripts/sync-appraise-base-template.ts +124 -105
- package/templates/default/scripts/sync-environments.ts +8 -5
- package/templates/default/scripts/sync-locator-groups.ts +7 -10
- package/templates/default/scripts/sync-locators.ts +5 -9
- package/templates/default/scripts/sync-modules.ts +9 -17
- package/templates/default/scripts/sync-tags.ts +2 -2
- package/templates/default/scripts/sync-template-step-groups.ts +16 -6
- package/templates/default/scripts/sync-template-steps.ts +16 -5
- package/templates/default/scripts/sync-test-cases.ts +6 -3
- package/templates/default/scripts/sync-test-suites.ts +7 -4
- package/templates/default/src/actions/environments/environment-actions.ts +6 -23
- package/templates/default/src/actions/locator/locator-actions.ts +36 -93
- package/templates/default/src/actions/locator-groups/locator-group-actions.ts +24 -78
- package/templates/default/src/actions/modules/module-actions.ts +4 -2
- package/templates/default/src/actions/tags/tag-actions.ts +4 -1
- package/templates/default/src/actions/template-step/template-step-actions.ts +10 -101
- package/templates/default/src/actions/template-step-group/template-step-group-actions.ts +31 -130
- package/templates/default/src/actions/test-case/test-case-actions.ts +31 -94
- package/templates/default/src/actions/test-run/test-run-actions.ts +11 -13
- package/templates/default/src/actions/test-suite/test-suite-actions.ts +29 -82
- package/templates/default/src/app/(base)/locator-groups/page.tsx +1 -3
- package/templates/default/src/app/(base)/reports/page.tsx +1 -1
- package/templates/default/src/app/(base)/reports/test-cases/page.tsx +2 -2
- package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table-columns.tsx +1 -1
- package/templates/default/src/app/(base)/tags/page.tsx +2 -2
- package/templates/default/src/app/(base)/template-steps/page.tsx +1 -2
- package/templates/default/src/app/(base)/test-runs/page.tsx +2 -2
- package/templates/default/src/app/api/test-runs/[runId]/logs/route.ts +2 -1
- package/templates/default/src/app/api/test-runs/[runId]/trace/[testCaseId]/route.ts +2 -1
- package/templates/default/src/app/page.tsx +4 -5
- package/templates/default/src/components/diagram/dynamic-parameters.tsx +76 -40
- package/templates/default/src/components/diagram/options-header-node.tsx +1 -1
- package/templates/default/src/components/ui/data-table.tsx +33 -39
- package/templates/default/src/lib/automation/paths.ts +181 -0
- package/templates/default/src/lib/automation/projection-service.ts +230 -0
- package/templates/default/src/lib/environment-file-utils.ts +14 -51
- package/templates/default/src/lib/executor/local-executor-adapter.ts +101 -0
- package/templates/default/src/lib/executor/types.ts +24 -0
- package/templates/default/src/lib/feature-file-generator.ts +22 -112
- package/templates/default/src/lib/locator-group-file-utils.ts +57 -120
- package/templates/default/src/lib/process/task-spawner.ts +236 -0
- package/templates/default/src/lib/template-sync-utils.d.ts +7 -0
- package/templates/default/src/lib/template-sync-utils.d.ts.map +1 -0
- package/templates/default/src/lib/template-sync-utils.js +47 -0
- package/templates/default/src/lib/template-sync-utils.js.map +1 -0
- package/templates/default/src/lib/template-sync-utils.ts +63 -0
- package/templates/default/src/lib/test-run/process-manager.ts +9 -87
- package/templates/default/src/lib/test-run/test-run-executor.ts +7 -136
- package/templates/default/src/lib/test-run/winston-logger.ts +6 -35
- package/templates/default/src/lib/utils/template-step-file-generator.ts +22 -85
- package/templates/default/src/lib/utils/template-step-file-manager-intelligent.ts +7 -22
- package/templates/default/public/favicon.ico +0 -0
- package/templates/default/src/tests/executor.ts +0 -80
- package/templates/default/src/tests/mapping/locator-map.json +0 -1
- package/templates/default/src/tests/steps/actions/wait.step.ts +0 -107
- package/templates/default/src/tests/support/parameter-types.ts +0 -12
- package/templates/default/src/tests/utils/cache.util.ts +0 -260
- package/templates/default/src/tests/utils/cli.util.ts +0 -177
- package/templates/default/src/tests/utils/environment.util.ts +0 -65
- package/templates/default/src/tests/utils/random-data.util.ts +0 -45
- 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.
|
|
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": "
|
|
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
|
|
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
|
|
16
|
-
"setup": "npm run install-dependencies && npm run setup
|
|
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,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 {
|
|
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
|
-
|
|
69
|
-
await
|
|
70
|
-
|
|
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
|
|
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
|
|
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(
|
|
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?: (
|
|
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 (
|
|
30
|
-
lastErr =
|
|
31
|
-
if (i === retries || !shouldRetry(
|
|
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
|
|
42
|
-
return includeSearch ? `${
|
|
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 =
|
|
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)
|
|
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
|
|
95
|
+
const observer = new MutationObserver(() => {
|
|
101
96
|
lastMutation = Date.now()
|
|
102
97
|
})
|
|
103
|
-
|
|
98
|
+
observer.observe(document, { subtree: true, childList: true, attributes: true })
|
|
104
99
|
|
|
105
100
|
const tick = () => {
|
|
106
101
|
if (Date.now() - lastMutation >= quietMs) {
|
|
107
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
171
|
-
if (
|
|
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:
|
|
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:
|
|
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)
|
|
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)
|
|
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)
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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,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
|
+
}
|