Skip to content

Commit

Permalink
Introduce parameters and placeholders (#15)
Browse files Browse the repository at this point in the history
* Introduce parameter and placeholder

* Bump version

* Format

* Remove unused property

* Test nested property access

* Remove jest-expect dependency
  • Loading branch information
abdala authored Oct 17, 2024
1 parent 6377961 commit 9d84e89
Show file tree
Hide file tree
Showing 19 changed files with 271 additions and 132 deletions.
5 changes: 1 addition & 4 deletions assert/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cgauge/assert",
"version": "0.4.0",
"version": "0.5.0",
"description": "Extra assert library",
"type": "module",
"repository": {
Expand All @@ -14,9 +14,6 @@
],
"author": "Abdala Cerqueira",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@jest/expect-utils": "^29.7.0"
},
"scripts": {
"build": "rm -rf dist && tsc",
"test": "tsx --test test/*.test.ts",
Expand Down
44 changes: 29 additions & 15 deletions assert/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
import nodeAssert from 'node:assert'
import {equals, getObjectKeys, iterableEquality, subsetEquality} from '@jest/expect-utils'

interface ExtraAssertions {
objectContains<T extends Record<string | symbol, unknown>>(
actual: Record<string | symbol, unknown>,
objectContains<T extends Record<string, any>>(
actual: Record<string, any>,
expected: T,
message?: string | Error,
): asserts actual is Partial<T>
}

const assertions: ExtraAssertions = {
objectContains<T extends Record<string | symbol, unknown>>(
actual: Record<string | symbol, unknown>,
objectContains<T extends Record<string, any>>(
actual: Record<string, any>,
expected: T,
message?: string | Error,
): asserts actual is Partial<T> {
const actualKeys = getObjectKeys(actual)
const expectedKeys = getObjectKeys(expected)

if (typeof actual !== 'object' || typeof expected !== 'object') {
throw new Error('Both actual and expected values must be objects');
}

const expectedKeys = Object.keys(expected);

for (const key of expectedKeys) {
if (!actualKeys.includes(key) || !equals(actual[key], expected[key], [iterableEquality, subsetEquality])) {
nodeAssert.deepEqual(
actual,
expected,
message ??
`Invalid key [${key.toString()}]\n
if (key in actual) {
if (typeof actual[key] !== typeof expected[key]) {
throw new Error(`Type mismatch for key '${String(key)}'. Expected ${typeof expected[key]} but got ${typeof actual[key]}. ${message}`);
}

if (typeof expected[key] === 'object') {
try {
assertions.objectContains(actual[key], expected[key], message);
} catch (e: any) {
throw new Error(`Error asserting key '${String(key)}': ${e.message}`);
}
} else if (actual[key] !== expected[key]) {
nodeAssert.deepEqual(
actual,
expected,
message ??
`Invalid key [${key.toString()}]\n
${JSON.stringify(actual, undefined, 2)}\n
${JSON.stringify(expected, undefined, 2)}`,
)
)
}
}
}
},
Expand Down
8 changes: 4 additions & 4 deletions dtc-aws-plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cgauge/dtc-aws-plugin",
"version": "0.4.0",
"version": "0.5.0",
"description": "AWS plugin for Declarative TestCases",
"repository": {
"type": "git",
Expand All @@ -15,9 +15,9 @@
"author": "Abdala Cerqueira",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@cgauge/assert": "^0.4.0",
"@cgauge/dtc": "^0.4.0",
"@cgauge/nock-aws": "^0.4.0",
"@cgauge/assert": "^0.5.0",
"@cgauge/dtc": "^0.5.0",
"@cgauge/nock-aws": "^0.5.0",
"@aws-sdk/client-dynamodb": "^3.645.0",
"@aws-sdk/client-eventbridge": "^3.645.0",
"@aws-sdk/client-lambda": "^3.645.0",
Expand Down
6 changes: 3 additions & 3 deletions dtc-mysql-plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cgauge/dtc-mysql-plugin",
"version": "0.4.0",
"version": "0.5.0",
"description": "MySQL plugin for Declarative TestCases",
"repository": {
"type": "git",
Expand All @@ -15,8 +15,8 @@
"author": "Abdala Cerqueira",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@cgauge/assert": "^0.4.0",
"@cgauge/dtc": "^0.4.0",
"@cgauge/assert": "^0.5.0",
"@cgauge/dtc": "^0.5.0",
"mysql2": "^3.11.0",
"node-sql-parser": "^5.1.0"
},
Expand Down
6 changes: 3 additions & 3 deletions dtc-playwright-plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cgauge/dtc-playwright-plugin",
"version": "0.4.0",
"version": "0.5.0",
"description": "Playwright plugin for Declarative TestCases",
"repository": {
"type": "git",
Expand All @@ -15,8 +15,8 @@
"author": "Abdala Cerqueira",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@cgauge/assert": "^0.4.0",
"@cgauge/dtc": "^0.4.0",
"@cgauge/assert": "^0.5.0",
"@cgauge/dtc": "^0.5.0",
"@playwright/test": "^1.47.0"
},
"scripts": {
Expand Down
7 changes: 5 additions & 2 deletions dtc-playwright-plugin/src/browser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {executeTestCase, resolveConfig} from '@cgauge/dtc'
import {executeTestCase, resolveConfig, loadTestCases} from '@cgauge/dtc'
import {test} from '@playwright/test'

const path = process.env.DTC_PLAYWRIGHT_PATH
const config = String(process.env.DTC_PLAYWRIGHT_CONFIG)

const {testCaseExecutions, plugins} = await resolveConfig(path, config)
const {loader, plugins, testRegex} = await resolveConfig(config)
const projectPath = process.cwd()

const testCaseExecutions = await loadTestCases(projectPath, loader, testRegex, path)

for (const {filePath, testCase} of testCaseExecutions) {
test(testCase.name, async ({page}) => executeTestCase(testCase, plugins, filePath, {page}))
Expand Down
4 changes: 2 additions & 2 deletions dtc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cgauge/dtc",
"version": "0.4.0",
"version": "0.5.0",
"description": "Declarative TestCases",
"repository": {
"type": "git",
Expand All @@ -18,7 +18,7 @@
"author": "Abdala Cerqueira",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@cgauge/assert": "^0.4.0",
"@cgauge/assert": "^0.5.0",
"cleye": "^1.3.2",
"nock": "^14.0.0-beta.11"
},
Expand Down
6 changes: 5 additions & 1 deletion dtc/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {cli} from 'cleye'
import {resolveConfig} from './config.js'
import {loadTestCases} from './loader.js'

const argv = cli({
name: 'cli.ts',
Expand All @@ -17,8 +18,11 @@ const argv = cli({

const config = argv.flags.config
const filePath = argv._[0] ?? null
const projectPath = process.cwd()

const {testCaseExecutions, plugins, runner} = await resolveConfig(filePath, config)
const {loader, plugins, runner, testRegex} = await resolveConfig(config)

const testCaseExecutions = await loadTestCases(projectPath, loader, testRegex, filePath)

if (!runner) {
throw new Error(`No test runner found`)
Expand Down
53 changes: 3 additions & 50 deletions dtc/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import type {Runner, Loader, TestCaseExecution} from './domain'
import type {Runner, Loader} from './domain'
import {defaultLoader, defaultPlugins, defaultTestRunner} from './index.js'
import {readdirSync, statSync} from 'fs'
import {join} from 'path'

export type Config = {
plugins: string[]
loader: Loader
runner: Runner
testDir: string
testRegex: string
}

export const resolveConfig = async (filePath?: string, configPath?: string) => {
const projectPath = process.cwd()
export const resolveConfig = async (configPath?: string) => {
let runner: Runner = defaultTestRunner
let plugins = defaultPlugins
let loader = defaultLoader
Expand All @@ -32,49 +28,6 @@ export const resolveConfig = async (filePath?: string, configPath?: string) => {
testRegex = customTestRegex ?? testRegex
}

const testCaseExecutions = await getTestCases(projectPath, loader, testRegex, filePath)

return {testCaseExecutions, plugins, runner}
return {loader, plugins, runner, testRegex}
}

const generateTestCaseExecution = async (filePath: string, loader: Config['loader']): Promise<TestCaseExecution> => ({
filePath,
testCase: await loader(filePath),
})

const getTestCases = async (
projectPath: string,
loader: Config['loader'] | null,
testRegex: RegExp,
filePath?: string,
): Promise<TestCaseExecution[]> => {
if (filePath) {
return loader
? [await generateTestCaseExecution(filePath, loader)]
: [await generateTestCaseExecution(filePath, defaultLoader)]
}

const files = loadTestFiles(projectPath, testRegex)

return await Promise.all(
files.map((file) =>
loader ? generateTestCaseExecution(file, loader) : generateTestCaseExecution(file, defaultLoader),
),
)
}

export const loadTestFiles = (currentPath: string, testRegex: RegExp): string[] =>
readdirSync(currentPath)
.map((file) => {
const filePath = join(currentPath, file)

if (statSync(filePath).isDirectory()) {
return loadTestFiles(filePath, testRegex)
} else if (testRegex.test(filePath)) {
return filePath
}

return null
})
.filter((item) => item !== null)
.flat()
1 change: 1 addition & 0 deletions dtc/src/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type TestCase = {
debug?: boolean
retry?: number
delay?: number
parameters?: Record<string, unknown> | Record<string, unknown>[]
arrange?: Record<string, unknown>
act?: Record<string, unknown>
assert?: unknown
Expand Down
1 change: 1 addition & 0 deletions dtc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import test from 'node:test'
export type * from './domain'
export * from './utils.js'
export * from './config.js'
export * from './loader.js'
export * as DisableNetConnectPlugin from './plugins/disable-net-connect-plugin.js'
export * as FunctionCallPlugin from './plugins/function-call-plugin.js'

Expand Down
111 changes: 111 additions & 0 deletions dtc/src/loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type {TestCaseExecution, Loader} from './domain'
import {readdir, stat} from 'node:fs/promises'
import {join} from 'path'

const generateTestCaseExecution = async (filePath: string, loader: Loader): Promise<TestCaseExecution> => ({
filePath,
testCase: await loader(filePath),
})

const loadTestFiles = async (currentPath: string, testRegex: RegExp): Promise<string[]> => {
const files = await readdir(currentPath)

const recursiveLoad = files.map(async (file) => {
const filePath = join(currentPath, file)
const stats = await stat(filePath)
if (stats.isDirectory()) {
return loadTestFiles(filePath, testRegex)
} else if (testRegex.test(filePath)) {
return filePath
}

return null
})

const result = await Promise.all(recursiveLoad)

return result.filter((item) => item !== null).flat()
}

const recursiveMap = (obj: any, callback: (v: any) => any): any => {
if (Array.isArray(obj)) {
return obj.map((element) => recursiveMap(element, callback))
}

if (typeof obj === 'object' && obj !== null) {
return Object.keys(obj).reduce((acc, key) => {
acc[key] = recursiveMap(obj[key], callback)
return acc
}, {} as Record<string, unknown>)
}

return callback(obj)
}

const replacePlaceholders = (obj: any, params: any) =>
recursiveMap(obj, (value: string | number | boolean) => {
if (typeof value !== 'string') {
return value
}

return value.replace(/\${(.*?)}/g, (match, group) => {
const path = group.split('.')
let value = params
for (const prop of path) {
if (value && typeof value === 'object') {
value = value[prop]
} else {
return match
}
}
return value
})
})

const resolveParameters = (testCaseExecution: TestCaseExecution): TestCaseExecution[] => {
if (testCaseExecution.testCase.parameters) {
const params = testCaseExecution.testCase.parameters

if (Array.isArray(params)) {
return params.map((param, i) => {
const resolvedParams = replacePlaceholders(param, param)

return {
filePath: testCaseExecution.filePath,
testCase: {
...replacePlaceholders(testCaseExecution.testCase, resolvedParams),
name: `${testCaseExecution.testCase.name} (provider ${i})`,
},
}
})
}

const resolvedParams = replacePlaceholders(params, params)

return [
{
filePath: testCaseExecution.filePath,
testCase: replacePlaceholders(testCaseExecution.testCase, resolvedParams),
},
]
}

return [testCaseExecution]
}

export const loadTestCases = async (
projectPath: string,
loader: Loader,
testRegex: RegExp,
filePath?: string,
): Promise<TestCaseExecution[]> => {
if (filePath) {
const testCaseExecutions = await generateTestCaseExecution(filePath, loader)
return resolveParameters(testCaseExecutions)
}

const files = await loadTestFiles(projectPath, testRegex)
const testCaseExecutions = await Promise.all(files.map((file) => generateTestCaseExecution(file, loader)))

return testCaseExecutions.map(resolveParameters).flat()
}
Loading

0 comments on commit 9d84e89

Please sign in to comment.