diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..3db17ec --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + plugins: ['@typescript-eslint'], + env: { + node: true, + jest: true, + es6: true, + }, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-console': 'off', + }, + ignorePatterns: ['dist/', 'node_modules/', '*.js'], +}; diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..1ef8ac1 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: Lint and Type Check + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '18.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + - name: Type check + run: npm run type-check diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..3867a0f --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +npm run lint diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..214c29d --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/package-lock.json b/package-lock.json index 958c8cf..b170ea2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,17 @@ }, "devDependencies": { "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "husky": "^9.1.7", "jest": "^29.7.0", + "prettier": "^3.2.5", "ts-jest": "^29.2.4", "tsc-alias": "^1.8.10", - "typescript": "^5.6.2" + "typescript": "^5.3.3" } }, "node_modules/@ampproject/remapping": { @@ -533,6 +540,156 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -1304,6 +1461,19 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1424,6 +1594,13 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.10.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", @@ -1434,6 +1611,13 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1458,6 +1642,316 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", + "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2153,6 +2647,13 @@ "node": ">=4.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2205,6 +2706,19 @@ "node": ">=8" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -2257,34 +2771,307 @@ "once": "^1.4.0" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { - "is-arrayish": "^0.2.1" + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "MIT", + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -2301,6 +3088,52 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2360,6 +3193,20 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -2384,6 +3231,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", @@ -2404,6 +3258,19 @@ "bser": "2.1.1" } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -2464,6 +3331,28 @@ "node": ">=8" } }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -2624,6 +3513,13 @@ "dev": true, "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2664,6 +3560,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2694,6 +3606,33 @@ "node": ">= 4" } }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2837,6 +3776,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3590,6 +4539,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -3597,6 +4553,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3610,6 +4580,16 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3630,6 +4610,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -3657,6 +4651,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3920,6 +4921,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3975,6 +4994,19 @@ "node": ">=6" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -4128,6 +5160,45 @@ "node": ">=10" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -4180,6 +5251,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -4361,6 +5442,23 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4736,6 +5834,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tar-fs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", @@ -4779,6 +5894,13 @@ "node": ">=8" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -4799,6 +5921,19 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-jest": { "version": "29.2.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", @@ -4883,8 +6018,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "devOptional": true, + "license": "0BSD" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -4898,6 +6033,19 @@ "node": "*" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4973,6 +6121,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5020,6 +6178,16 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 44ecc44..434258c 100644 --- a/package.json +++ b/package.json @@ -26,17 +26,28 @@ "build": "tsc && tsc-alias", "test": "jest", "bump-version": "npm version patch && git push && git push --tags", - "release": "npm run test && npm run bump-version && npm run build && npm publish" + "release": "npm run test && npm run bump-version && npm run build && npm publish", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "type-check": "tsc --noEmit", + "prepare": "husky" }, "bugs": { "url": "https://github.com/wix-incubator/detox-copilot/issues" }, "devDependencies": { "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "husky": "^9.1.7", "jest": "^29.7.0", + "prettier": "^3.2.5", "ts-jest": "^29.2.4", "tsc-alias": "^1.8.10", - "typescript": "^5.6.2" + "typescript": "^5.3.3" }, "dependencies": { "blockhash-core": "^0.1.0", diff --git a/src/Copilot.test.ts b/src/Copilot.test.ts index 278defa..8b10940 100644 --- a/src/Copilot.test.ts +++ b/src/Copilot.test.ts @@ -1,26 +1,29 @@ -import { Copilot } from '@/Copilot'; -import { CopilotStepPerformer } from '@/actions/CopilotStepPerformer'; -import { CopilotError } from '@/errors/CopilotError'; -import { Config, ScreenCapturerResult, PromptHandler, PilotStepReport} from '@/types'; -import { mockCache, mockedCacheFile } from './test-utils/cache'; -import { ScreenCapturer } from '@/utils/ScreenCapturer'; +import { Copilot } from "@/Copilot"; +import { CopilotStepPerformer } from "@/actions/CopilotStepPerformer"; +import { CopilotError } from "@/errors/CopilotError"; +import { + Config, + ScreenCapturerResult, + PromptHandler, + PilotStepReport, +} from "@/types"; +import { mockCache, mockedCacheFile } from "./test-utils/cache"; +import { ScreenCapturer } from "@/utils/ScreenCapturer"; import { bazCategory, barCategory2, barCategory1, dummyContext, -} from './test-utils/APICatalogTestUtils'; -import { PilotPerformer } from './actions/PilotPerformer'; +} from "./test-utils/APICatalogTestUtils"; +import { PilotPerformer } from "./actions/PilotPerformer"; -jest.mock('@/actions/CopilotStepPerformer'); -jest.mock('@/utils/ScreenCapturer'); -jest.mock('fs'); +jest.mock("@/actions/CopilotStepPerformer"); +jest.mock("@/utils/ScreenCapturer"); +jest.mock("fs"); -const INTENT = 'tap button'; -const SNAPSHOT_DATA = 'snapshot_data'; -const VIEW_HIERARCHY = 'hash'; +const INTENT = "tap button"; -describe('Copilot', () => { +describe("Copilot", () => { let mockConfig: Config; let mockPromptHandler: jest.Mocked; let mockFrameworkDriver: any; @@ -30,40 +33,44 @@ describe('Copilot', () => { beforeEach(() => { mockPromptHandler = { runPrompt: jest.fn(), - isSnapshotImageSupported: jest.fn() + isSnapshotImageSupported: jest.fn(), } as any; mockFrameworkDriver = { apiCatalog: { context: {}, - categories: [] + categories: [], }, captureSnapshotImage: jest.fn(), - captureViewHierarchyString: jest.fn() + captureViewHierarchyString: jest.fn(), }; mockPilotPerformer = { - perform: jest.fn() + perform: jest.fn(), } as any; mockConfig = { promptHandler: mockPromptHandler, - frameworkDriver: mockFrameworkDriver + frameworkDriver: mockFrameworkDriver, }; - jest.spyOn(PilotPerformer.prototype, 'perform').mockImplementation(mockPilotPerformer.perform); + jest + .spyOn(PilotPerformer.prototype, "perform") + .mockImplementation(mockPilotPerformer.perform); screenCapture = { - snapshot: 'base64-encoded-image', + snapshot: "base64-encoded-image", viewHierarchy: ''), + captureSnapshotImage: jest + .fn() + .mockResolvedValue(getSnapshotImage("baseline")), + captureViewHierarchyString: jest + .fn() + .mockResolvedValue(""), apiCatalog: { context: {}, - categories: [] - } + categories: [], + }, }; mockPromptHandler = { runPrompt: jest.fn(), - isSnapshotImageSupported: jest.fn().mockReturnValue(true) + isSnapshotImageSupported: jest.fn().mockReturnValue(true), }; - mockedCachedSnapshotHash = await new SnapshotComparator().generateHashes(getSnapshotImage("baseline")); + mockedCachedSnapshotHash = await new SnapshotComparator().generateHashes( + getSnapshotImage("baseline"), + ); mockCache(); (crypto.createHash as jest.Mock).mockReturnValue({ update: jest.fn().mockReturnValue({ - digest: jest.fn().mockReturnValue('hash'), + digest: jest.fn().mockReturnValue("hash"), }), }); }); @@ -53,12 +69,12 @@ describe('Copilot Integration Tests', () => { (Copilot as any).instance = undefined; }); - describe('Initialization', () => { - it('should throw an error when perform is called before initialization', async () => { - await expect(copilot.perform('Some action')).rejects.toThrow(); + describe("Initialization", () => { + it("should throw an error when perform is called before initialization", async () => { + await expect(copilot.perform("Some action")).rejects.toThrow(); }); - it('should initialize successfully', () => { + it("should initialize successfully", () => { expect(() => { copilot.init({ frameworkDriver: mockFrameworkDriver, @@ -67,11 +83,11 @@ describe('Copilot Integration Tests', () => { }).not.toThrow(); }); - it('should return false when isInitialized is called before initialization', () => { + it("should return false when isInitialized is called before initialization", () => { expect(copilot.isInitialized()).toBe(false); }); - it('should return true when isInitialized is called after initialization', () => { + it("should return true when isInitialized is called after initialization", () => { copilot.init({ frameworkDriver: mockFrameworkDriver, promptHandler: mockPromptHandler, @@ -81,7 +97,7 @@ describe('Copilot Integration Tests', () => { }); }); - describe('Single Step Operations', () => { + describe("Single Step Operations", () => { beforeEach(() => { copilot.init({ frameworkDriver: mockFrameworkDriver, @@ -90,51 +106,65 @@ describe('Copilot Integration Tests', () => { copilot.start(); }); - it('should successfully perform an action', async () => { - mockPromptHandler.runPrompt.mockResolvedValue('// No operation'); - await expect(copilot.perform('Tap on the login button')).resolves.not.toThrow(); + it("should successfully perform an action", async () => { + mockPromptHandler.runPrompt.mockResolvedValue("// No operation"); + await expect( + copilot.perform("Tap on the login button"), + ).resolves.not.toThrow(); expect(mockFrameworkDriver.captureSnapshotImage).toHaveBeenCalled(); expect(mockFrameworkDriver.captureViewHierarchyString).toHaveBeenCalled(); expect(mockPromptHandler.runPrompt).toHaveBeenCalledWith( - expect.stringContaining('Tap on the login button'), - getSnapshotImage("baseline") + expect.stringContaining("Tap on the login button"), + getSnapshotImage("baseline"), ); }); - it('should successfully perform an assertion', async () => { - mockPromptHandler.runPrompt.mockResolvedValue('// No operation'); + it("should successfully perform an assertion", async () => { + mockPromptHandler.runPrompt.mockResolvedValue("// No operation"); - await expect(copilot.perform('The welcome message should be visible')).resolves.not.toThrow(); + await expect( + copilot.perform("The welcome message should be visible"), + ).resolves.not.toThrow(); expect(mockFrameworkDriver.captureSnapshotImage).toHaveBeenCalled(); expect(mockFrameworkDriver.captureViewHierarchyString).toHaveBeenCalled(); expect(mockPromptHandler.runPrompt).toHaveBeenCalledWith( - expect.stringContaining('The welcome message should be visible'), - getSnapshotImage("baseline") + expect.stringContaining("The welcome message should be visible"), + getSnapshotImage("baseline"), ); }); - it('should handle errors during action execution', async () => { - mockPromptHandler.runPrompt.mockResolvedValue('throw new Error("Element not found");'); + it("should handle errors during action execution", async () => { + mockPromptHandler.runPrompt.mockResolvedValue( + 'throw new Error("Element not found");', + ); - await expect(copilot.perform('Tap on a non-existent button')).rejects.toThrow('Element not found'); + await expect( + copilot.perform("Tap on a non-existent button"), + ).rejects.toThrow("Element not found"); }); - it('should handle errors during assertion execution', async () => { - mockPromptHandler.runPrompt.mockResolvedValue('throw new Error("Element not found");'); + it("should handle errors during assertion execution", async () => { + mockPromptHandler.runPrompt.mockResolvedValue( + 'throw new Error("Element not found");', + ); - await expect(copilot.perform('The welcome message should be visible')).rejects.toThrow('Element not found'); + await expect( + copilot.perform("The welcome message should be visible"), + ).rejects.toThrow("Element not found"); }); - it('should handle errors during code evaluation', async () => { - mockPromptHandler.runPrompt.mockResolvedValue('foobar'); + it("should handle errors during code evaluation", async () => { + mockPromptHandler.runPrompt.mockResolvedValue("foobar"); - await expect(copilot.perform('The welcome message should be visible')).rejects.toThrow(/foobar is not defined/); + await expect( + copilot.perform("The welcome message should be visible"), + ).rejects.toThrow(/foobar is not defined/); }); }); - describe('Multiple Step Operations', () => { + describe("Multiple Step Operations", () => { beforeEach(() => { copilot.init({ frameworkDriver: mockFrameworkDriver, @@ -143,45 +173,51 @@ describe('Copilot Integration Tests', () => { copilot.start(); }); - it('should perform multiple steps using spread operator', async () => { + it("should perform multiple steps using spread operator", async () => { mockPromptHandler.runPrompt - .mockResolvedValueOnce('// Tap login button') - .mockResolvedValueOnce('// Enter username') - .mockResolvedValueOnce('// Enter password'); + .mockResolvedValueOnce("// Tap login button") + .mockResolvedValueOnce("// Enter username") + .mockResolvedValueOnce("// Enter password"); await copilot.perform( - 'Tap on the login button', - 'Enter username "testuser"', - 'Enter password "password123"' + "Tap on the login button", + 'Enter username "testuser"', + 'Enter password "password123"', ); expect(mockPromptHandler.runPrompt).toHaveBeenCalledTimes(3); expect(mockFrameworkDriver.captureSnapshotImage).toHaveBeenCalledTimes(3); - expect(mockFrameworkDriver.captureViewHierarchyString).toHaveBeenCalledTimes(3); + expect( + mockFrameworkDriver.captureViewHierarchyString, + ).toHaveBeenCalledTimes(3); }); - it('should handle errors in multiple steps and stop execution', async () => { + it("should handle errors in multiple steps and stop execution", async () => { mockPromptHandler.runPrompt - .mockResolvedValueOnce('// Tap login button') - .mockResolvedValueOnce('throw new Error("Username field not found");') - .mockResolvedValueOnce('throw new Error("Username field not found - second");') - .mockResolvedValueOnce('// Enter password'); + .mockResolvedValueOnce("// Tap login button") + .mockResolvedValueOnce('throw new Error("Username field not found");') + .mockResolvedValueOnce( + 'throw new Error("Username field not found - second");', + ) + .mockResolvedValueOnce("// Enter password"); await expect( - copilot.perform( - 'Tap on the login button', - 'Enter username "testuser"', - 'Enter password "password123"' - ) - ).rejects.toThrow('Username field not found'); + copilot.perform( + "Tap on the login button", + 'Enter username "testuser"', + 'Enter password "password123"', + ), + ).rejects.toThrow("Username field not found"); expect(mockPromptHandler.runPrompt).toHaveBeenCalledTimes(3); expect(mockFrameworkDriver.captureSnapshotImage).toHaveBeenCalledTimes(2); - expect(mockFrameworkDriver.captureViewHierarchyString).toHaveBeenCalledTimes(2); + expect( + mockFrameworkDriver.captureViewHierarchyString, + ).toHaveBeenCalledTimes(2); }); }); - describe('Error Handling', () => { + describe("Error Handling", () => { beforeEach(() => { copilot.init({ frameworkDriver: mockFrameworkDriver, @@ -190,26 +226,36 @@ describe('Copilot Integration Tests', () => { copilot.start(); }); - it('should throw error when PromptHandler fails', async () => { - mockPromptHandler.runPrompt.mockRejectedValue(new Error('API error')); + it("should throw error when PromptHandler fails", async () => { + mockPromptHandler.runPrompt.mockRejectedValue(new Error("API error")); - await expect(copilot.perform('Perform action')).rejects.toThrow('API error'); + await expect(copilot.perform("Perform action")).rejects.toThrow( + "API error", + ); }); - it('should throw error when captureSnapshotImage() fails', async () => { - mockFrameworkDriver.captureSnapshotImage.mockRejectedValue(new Error('Snapshot error')); + it("should throw error when captureSnapshotImage() fails", async () => { + mockFrameworkDriver.captureSnapshotImage.mockRejectedValue( + new Error("Snapshot error"), + ); - await expect(copilot.perform('Perform action')).rejects.toThrow('Snapshot error'); + await expect(copilot.perform("Perform action")).rejects.toThrow( + "Snapshot error", + ); }); - it('should throw error when captureViewHierarchyString() fails', async () => { - mockFrameworkDriver.captureViewHierarchyString.mockRejectedValue(new Error('Hierarchy error')); + it("should throw error when captureViewHierarchyString() fails", async () => { + mockFrameworkDriver.captureViewHierarchyString.mockRejectedValue( + new Error("Hierarchy error"), + ); - await expect(copilot.perform('Perform action')).rejects.toThrow('Hierarchy error'); + await expect(copilot.perform("Perform action")).rejects.toThrow( + "Hierarchy error", + ); }); }); - describe('Context Management', () => { + describe("Context Management", () => { beforeEach(() => { copilot.init({ frameworkDriver: mockFrameworkDriver, @@ -218,46 +264,52 @@ describe('Copilot Integration Tests', () => { copilot.start(); }); - it('should reset context when end is called', async () => { - mockPromptHandler.runPrompt.mockResolvedValueOnce('// Login action'); - await copilot.perform('Log in to the application'); + it("should reset context when end is called", async () => { + mockPromptHandler.runPrompt.mockResolvedValueOnce("// Login action"); + await copilot.perform("Log in to the application"); copilot.end(); copilot.start(); - mockPromptHandler.runPrompt.mockResolvedValueOnce('// New action after reset'); - await copilot.perform('Perform action after reset'); + mockPromptHandler.runPrompt.mockResolvedValueOnce( + "// New action after reset", + ); + await copilot.perform("Perform action after reset"); expect(mockPromptHandler.runPrompt).toHaveBeenCalledTimes(2); - expect(mockPromptHandler.runPrompt.mock.calls[1][0]).not.toContain('Log in to the application'); + expect(mockPromptHandler.runPrompt.mock.calls[1][0]).not.toContain( + "Log in to the application", + ); }); - it('should clear conversation history on reset', async () => { + it("should clear conversation history on reset", async () => { mockPromptHandler.runPrompt - .mockResolvedValueOnce('// Action 1') - .mockResolvedValueOnce('// Action 2'); + .mockResolvedValueOnce("// Action 1") + .mockResolvedValueOnce("// Action 2"); - await copilot.perform('Action 1'); - await copilot.perform('Action 2'); + await copilot.perform("Action 1"); + await copilot.perform("Action 2"); - const lastCallArgsBeforeReset = mockPromptHandler.runPrompt.mock.calls[1][0]; - expect(lastCallArgsBeforeReset).toContain('Action 1'); - expect(lastCallArgsBeforeReset).toContain('Action 2'); + const lastCallArgsBeforeReset = + mockPromptHandler.runPrompt.mock.calls[1][0]; + expect(lastCallArgsBeforeReset).toContain("Action 1"); + expect(lastCallArgsBeforeReset).toContain("Action 2"); copilot.end(); copilot.start(); - mockPromptHandler.runPrompt.mockResolvedValueOnce('// New action'); - await copilot.perform('New action after reset'); + mockPromptHandler.runPrompt.mockResolvedValueOnce("// New action"); + await copilot.perform("New action after reset"); - const lastCallArgsAfterReset = mockPromptHandler.runPrompt.mock.calls[2][0]; - expect(lastCallArgsAfterReset).not.toContain('Action 1'); - expect(lastCallArgsAfterReset).not.toContain('Action 2'); - expect(lastCallArgsAfterReset).toContain('New action after reset'); + const lastCallArgsAfterReset = + mockPromptHandler.runPrompt.mock.calls[2][0]; + expect(lastCallArgsAfterReset).not.toContain("Action 1"); + expect(lastCallArgsAfterReset).not.toContain("Action 2"); + expect(lastCallArgsAfterReset).toContain("New action after reset"); }); }); - describe('Caching Behavior', () => { + describe("Caching Behavior", () => { beforeEach(() => { copilot.init({ frameworkDriver: mockFrameworkDriver, @@ -266,89 +318,95 @@ describe('Copilot Integration Tests', () => { copilot.start(); }); - it('should create cache file if it does not exist', async () => { - mockPromptHandler.runPrompt.mockResolvedValue('// Perform action'); + it("should create cache file if it does not exist", async () => { + mockPromptHandler.runPrompt.mockResolvedValue("// Perform action"); - await copilot.perform('Perform action'); + await copilot.perform("Perform action"); copilot.end(false); expect(mockedCacheFile).toEqual({ '{"step":"Perform action","previous":[]}': expect.arrayContaining([ - expect.objectContaining({ - code: '// Perform action', + expect.objectContaining({ + code: "// Perform action", snapshotHash: expect.any(Object), viewHierarchy: expect.any(String), - }), + }), ]), }); }); - it('should read from existing cache file', async () => { + it("should read from existing cache file", async () => { mockCache({ - '{"step":"Cached action","previous":[]}': [{code:'// Cached action code', viewHierarchy: 'hash'}], + '{"step":"Cached action","previous":[]}': [ + { code: "// Cached action code", viewHierarchy: "hash" }, + ], }); - await copilot.perform('Cached action'); + await copilot.perform("Cached action"); expect(mockPromptHandler.runPrompt).not.toHaveBeenCalled(); }); - it('should use snapshot cache if available', async () => { + it("should use snapshot cache if available", async () => { mockCache({ - '{"step":"Cached action","previous":[]}': [{ - code:'// Cached action code', - viewHierarchy: 'WrongHash', + '{"step":"Cached action","previous":[]}': [ + { + code: "// Cached action code", + viewHierarchy: "WrongHash", snapshotHash: mockedCachedSnapshotHash, - }], + }, + ], }); - await copilot.perform('Cached action'); + await copilot.perform("Cached action"); expect(mockPromptHandler.runPrompt).not.toHaveBeenCalled(); }); - it('should update cache file after performing new action', async () => { - mockPromptHandler.runPrompt.mockResolvedValue('// New action code'); + it("should update cache file after performing new action", async () => { + mockPromptHandler.runPrompt.mockResolvedValue("// New action code"); - await copilot.perform('New action'); + await copilot.perform("New action"); copilot.end(); expect(mockedCacheFile).toEqual({ '{"step":"New action","previous":[]}': expect.arrayContaining([ expect.any(Object), - expect.objectContaining({ - code: '// New action code', + expect.objectContaining({ + code: "// New action code", snapshotHash: expect.any(Object), viewHierarchy: expect.any(String), - }), + }), ]), }); }); - it('should handle fs.readFileSync errors', async () => { + it("should handle fs.readFileSync errors", async () => { mockCache({}); // Set up an initial mocked file (fs.readFileSync as jest.Mock).mockImplementation(() => { - throw new Error('Read error'); + throw new Error("Read error"); }); - mockPromptHandler.runPrompt.mockResolvedValue('// New action code'); + mockPromptHandler.runPrompt.mockResolvedValue("// New action code"); - await copilot.perform('Action with read error'); + await copilot.perform("Action with read error"); expect(mockPromptHandler.runPrompt).toHaveBeenCalled(); }); - it('should handle fs.writeFileSync errors', async () => { + it("should handle fs.writeFileSync errors", async () => { mockCache(undefined); // No mocked file exists (fs.writeFileSync as jest.Mock).mockImplementation(() => { - throw new Error('Write error'); + throw new Error("Write error"); }); - mockPromptHandler.runPrompt.mockResolvedValue('// Action code'); + mockPromptHandler.runPrompt.mockResolvedValue("// Action code"); - await expect(copilot.perform('Action with write error')).resolves.not.toThrow(); + await expect( + copilot.perform("Action with write error"), + ).resolves.not.toThrow(); }); }); - describe('Feature Support', () => { + describe("Feature Support", () => { beforeEach(() => { copilot.init({ frameworkDriver: mockFrameworkDriver, @@ -357,22 +415,24 @@ describe('Copilot Integration Tests', () => { copilot.start(); }); - it('should work without snapshot images when not supported', async () => { + it("should work without snapshot images when not supported", async () => { mockPromptHandler.isSnapshotImageSupported.mockReturnValue(false); - mockPromptHandler.runPrompt.mockResolvedValue('// Perform action without snapshot'); + mockPromptHandler.runPrompt.mockResolvedValue( + "// Perform action without snapshot", + ); - await copilot.perform('Perform action without snapshot support'); + await copilot.perform("Perform action without snapshot support"); expect(mockFrameworkDriver.captureSnapshotImage).not.toHaveBeenCalled(); expect(mockFrameworkDriver.captureViewHierarchyString).toHaveBeenCalled(); expect(mockPromptHandler.runPrompt).toHaveBeenCalledWith( - expect.stringContaining('Perform action without snapshot support'), - undefined + expect.stringContaining("Perform action without snapshot support"), + undefined, ); }); }); - describe('API Catalog Extension', () => { + describe("API Catalog Extension", () => { beforeEach(() => { jest.clearAllMocks(); copilot.init({ @@ -382,9 +442,15 @@ describe('Copilot Integration Tests', () => { copilot.start(); }); - it('should call relevant functions to extend the catalog', () => { - const spyPromptCreator = jest.spyOn(PromptCreator.prototype, 'extendAPICategories'); - const spyCopilotStepPerformer = jest.spyOn(CopilotStepPerformer.prototype, 'extendJSContext'); + it("should call relevant functions to extend the catalog", () => { + const spyPromptCreator = jest.spyOn( + PromptCreator.prototype, + "extendAPICategories", + ); + const spyCopilotStepPerformer = jest.spyOn( + CopilotStepPerformer.prototype, + "extendJSContext", + ); copilot.extendAPICatalog([bazCategory]); expect(spyPromptCreator).toHaveBeenCalledTimes(1); @@ -395,18 +461,18 @@ describe('Copilot Integration Tests', () => { }); }); - describe('Pilot Method', () => { + describe("Pilot Method", () => { let mockFrameworkDriver: any; let mockPromptHandler: jest.Mocked; - + beforeEach(() => { jest.clearAllMocks(); - + mockPromptHandler = { runPrompt: jest.fn(), isSnapshotImageSupported: jest.fn(), } as any; - + mockFrameworkDriver = { apiCatalog: { context: {}, @@ -415,139 +481,146 @@ describe('Copilot Integration Tests', () => { captureSnapshotImage: jest.fn(), captureViewHierarchyString: jest.fn(), }; - + Copilot.init({ frameworkDriver: mockFrameworkDriver, promptHandler: mockPromptHandler, }); Copilot.getInstance().start(); }); - + afterEach(() => { - (Copilot as any)['instance'] = undefined; + (Copilot as any)["instance"] = undefined; }); - - it('should perform pilot flow and return a pilot report', async () => { - const goal = 'Complete the login flow'; + + it("should perform pilot flow and return a pilot report", async () => { + const goal = "Complete the login flow"; const mockPilotReport: PilotReport = { - summary: 'All steps completed successfully', + summary: "All steps completed successfully", goal: goal, steps: [ { - plan: { thoughts: 'First step thoughts', action: 'Tap on login button' }, - code: 'First step code output', + plan: { + thoughts: "First step thoughts", + action: "Tap on login button", + }, + code: "First step code output", review: { ux: { - summary: 'UX review for first step', + summary: "UX review for first step", findings: [], - score: '7/10', + score: "7/10", }, a11y: { - summary: 'Accessibility review for first step', + summary: "Accessibility review for first step", findings: [], - score: '8/10', + score: "8/10", }, }, }, ], review: { ux: { - summary: 'Overall UX review', + summary: "Overall UX review", findings: [], - score: '9/10', + score: "9/10", }, a11y: { - summary: 'Overall Accessibility review', + summary: "Overall Accessibility review", findings: [], - score: '9/10', + score: "9/10", }, }, }; const copilotInstance = Copilot.getInstance(); const spyPilotPerformerPerform = jest - .spyOn(copilotInstance['pilotPerformer'], 'perform') + .spyOn(copilotInstance["pilotPerformer"], "perform") .mockResolvedValue(mockPilotReport); - + const result = await copilotInstance.pilot(goal); - + expect(spyPilotPerformerPerform).toHaveBeenCalledTimes(1); expect(spyPilotPerformerPerform).toHaveBeenCalledWith(goal); expect(result).toEqual(mockPilotReport); }); - - it('should handle errors from pilotPerformer.perform', async () => { - const goal = 'Some goal that causes an error'; - - const errorMessage = 'Error during pilot execution'; + + it("should handle errors from pilotPerformer.perform", async () => { + const goal = "Some goal that causes an error"; + + const errorMessage = "Error during pilot execution"; const copilotInstance = Copilot.getInstance(); const spyPilotPerformerPerform = jest - .spyOn(copilotInstance['pilotPerformer'], 'perform') + .spyOn(copilotInstance["pilotPerformer"], "perform") .mockRejectedValue(new Error(errorMessage)); - + await expect(copilotInstance.pilot(goal)).rejects.toThrow(errorMessage); - + expect(spyPilotPerformerPerform).toHaveBeenCalledTimes(1); expect(spyPilotPerformerPerform).toHaveBeenCalledWith(goal); }); }); - - describe('Cache Modes', () => { + + describe("Cache Modes", () => { beforeEach(() => { - mockPromptHandler.runPrompt.mockResolvedValue('// No operation'); + mockPromptHandler.runPrompt.mockResolvedValue("// No operation"); }); - it('should use full cache mode by default', async () => { + it("should use full cache mode by default", async () => { copilot.init({ frameworkDriver: mockFrameworkDriver, - promptHandler: mockPromptHandler + promptHandler: mockPromptHandler, }); copilot.start(); - await copilot.perform('Tap on the login button'); + await copilot.perform("Tap on the login button"); copilot.end(); - const firstCacheValue = Object.values((mockedCacheFile as Record) || {})[0][0]; + const firstCacheValue = Object.values( + (mockedCacheFile as Record) || {}, + )[0][0]; - expect(firstCacheValue).toHaveProperty('viewHierarchy'); - expect(firstCacheValue).toHaveProperty('code'); - expect(firstCacheValue).toHaveProperty('snapshotHash'); + expect(firstCacheValue).toHaveProperty("viewHierarchy"); + expect(firstCacheValue).toHaveProperty("code"); + expect(firstCacheValue).toHaveProperty("snapshotHash"); }); - it('should not include view hierarchy in cache value when using lightweight mode', async () => { + it("should not include view hierarchy in cache value when using lightweight mode", async () => { copilot.init({ frameworkDriver: mockFrameworkDriver, promptHandler: mockPromptHandler, options: { - cacheMode: 'lightweight' - } + cacheMode: "lightweight", + }, }); copilot.start(); - await copilot.perform('Tap on the login button'); + await copilot.perform("Tap on the login button"); copilot.end(); - const firstCacheValue = Object.values((mockedCacheFile as Record) || {})[0][0]; + const firstCacheValue = Object.values( + (mockedCacheFile as Record) || {}, + )[0][0]; - expect(firstCacheValue).not.toHaveProperty('viewHierarchy'); - expect(firstCacheValue).toHaveProperty('code'); + expect(firstCacheValue).not.toHaveProperty("viewHierarchy"); + expect(firstCacheValue).toHaveProperty("code"); }); - it('should not use cache when cache mode is disabled', async () => { + it("should not use cache when cache mode is disabled", async () => { copilot.init({ frameworkDriver: mockFrameworkDriver, promptHandler: mockPromptHandler, options: { - cacheMode: 'disabled' - } + cacheMode: "disabled", + }, }); copilot.start(); // First call - await copilot.perform('Tap on the login button'); + await copilot.perform("Tap on the login button"); copilot.end(); // Second call with same intent copilot.start(); - await copilot.perform('Tap on the login button'); + await copilot.perform("Tap on the login button"); copilot.end(); // Should call runPrompt twice since cache is disabled @@ -555,51 +628,51 @@ describe('Copilot Integration Tests', () => { }); }); - describe('Analysis Modes', () => { + describe("Analysis Modes", () => { beforeEach(() => { - mockPromptHandler.runPrompt.mockResolvedValue('// No operation'); + mockPromptHandler.runPrompt.mockResolvedValue("// No operation"); }); - it('should perform fast analysis by default', async () => { + it("should perform fast analysis by default", async () => { copilot.init({ frameworkDriver: mockFrameworkDriver, - promptHandler: mockPromptHandler + promptHandler: mockPromptHandler, }); copilot.start(); - await copilot.perform('Tap on the login button'); + await copilot.perform("Tap on the login button"); copilot.end(); expect(mockPromptHandler.runPrompt).toHaveBeenCalledTimes(1); }); - it('should perform fast analysis when specified', async () => { + it("should perform fast analysis when specified", async () => { copilot.init({ frameworkDriver: mockFrameworkDriver, promptHandler: mockPromptHandler, options: { - analysisMode: 'fast' - } + analysisMode: "fast", + }, }); copilot.start(); - await copilot.perform('Tap on the login button'); + await copilot.perform("Tap on the login button"); copilot.end(); expect(mockPromptHandler.runPrompt).toHaveBeenCalledTimes(1); }); - it('should perform full analysis when specified', async () => { + it("should perform full analysis when specified", async () => { copilot.init({ frameworkDriver: mockFrameworkDriver, promptHandler: mockPromptHandler, options: { - analysisMode: 'full' - } + analysisMode: "full", + }, }); copilot.start(); - await copilot.perform('Tap on the login button'); + await copilot.perform("Tap on the login button"); copilot.end(); // requires several prompts to be run diff --git a/src/test-utils/APICatalogTestUtils.ts b/src/test-utils/APICatalogTestUtils.ts index 9babb9f..4b207aa 100644 --- a/src/test-utils/APICatalogTestUtils.ts +++ b/src/test-utils/APICatalogTestUtils.ts @@ -1,104 +1,118 @@ -import {TestingFrameworkAPICatalog} from "@/types"; +import { TestingFrameworkAPICatalog } from "@/types"; export const bazCategory = { - title: 'Custom Actions', - items: [ - { - signature: 'swipe(direction: string)', - description: 'Swipes in the specified direction.', - example: 'await swipe("up");', - guidelines: ['Use this method to scroll the screen.'] - } - ] + title: "Custom Actions", + items: [ + { + signature: "swipe(direction: string)", + description: "Swipes in the specified direction.", + example: 'await swipe("up");', + guidelines: ["Use this method to scroll the screen."], + }, + ], }; export const barCategory1 = { - title: 'Actions', - items: [ - { - signature: 'tapButton(id: string)', - description: 'Taps the button with the specified ID.', - example: 'await tapButton("submit");', - guidelines: ['Use this method to tap buttons.'] - } - ] + title: "Actions", + items: [ + { + signature: "tapButton(id: string)", + description: "Taps the button with the specified ID.", + example: 'await tapButton("submit");', + guidelines: ["Use this method to tap buttons."], + }, + ], }; export const barCategory2 = { - title: 'Actions', - items: [ - { - signature: 'swipe(direction: string)', - description: 'Swipes in the specified direction.', - example: 'await swipe("up");', - guidelines: ['Use this method to scroll the screen.'] - } - ] + title: "Actions", + items: [ + { + signature: "swipe(direction: string)", + description: "Swipes in the specified direction.", + example: 'await swipe("up");', + guidelines: ["Use this method to scroll the screen."], + }, + ], }; -export const dummyContext = {foo: jest.fn()}; -export const dummyBarContext1 = {bar: jest.fn()}; -export const dummyBarContext2 = {bar: jest.fn()}; +export const dummyContext = { foo: jest.fn() }; +export const dummyBarContext1 = { bar: jest.fn() }; +export const dummyBarContext2 = { bar: jest.fn() }; export const promptCreatorConstructorMockAPI: TestingFrameworkAPICatalog = { - context: {}, - name: 'Test Framework', - description: 'A testing framework for unit testing purposes', - categories: [ + context: {}, + name: "Test Framework", + description: "A testing framework for unit testing purposes", + categories: [ + { + title: "Actions", + items: [ { - title: 'Actions', - items: [{ - signature: 'tap(element: Element)', - description: 'Taps on the specified element.', - example: 'await element(by.id("button")).tap();', - guidelines: ['Ensure the element is tappable before using this method.'] - }, - { - signature: 'typeText(element: Element, text: string)', - description: 'Types the specified text into the element.', - example: 'await element(by.id("input")).typeText("Hello, World!");', - guidelines: ['Use this method only on text input elements.'] - }] + signature: "tap(element: Element)", + description: "Taps on the specified element.", + example: 'await element(by.id("button")).tap();', + guidelines: [ + "Ensure the element is tappable before using this method.", + ], }, { - title: 'Assertions', - items: [{ - signature: 'toBeVisible()', - description: 'Asserts that the element is visible on the screen.', - example: 'await expect(element(by.id("title"))).toBeVisible();', - guidelines: ['Consider scroll position when using this assertion.'] - }] + signature: "typeText(element: Element, text: string)", + description: "Types the specified text into the element.", + example: 'await element(by.id("input")).typeText("Hello, World!");', + guidelines: ["Use this method only on text input elements."], }, + ], + }, + { + title: "Assertions", + items: [ { - title: 'Assertions', - items: [{ - signature: 'toBeEnabled()', - description: 'Asserts that the element is enabled and can be interacted with.', - example: 'await expect(element(by.id("submitButton"))).toBeEnabled();', - guidelines: ['Ensure that the element is not disabled before performing actions.'] - }] + signature: "toBeVisible()", + description: "Asserts that the element is visible on the screen.", + example: 'await expect(element(by.id("title"))).toBeVisible();', + guidelines: ["Consider scroll position when using this assertion."], }, + ], + }, + { + title: "Assertions", + items: [ { - title: 'Matchers', - items: [ - { - signature: 'by.id(id: string)', - description: 'Matches elements by their ID attribute.', - example: 'element(by.id("uniqueId"))', - guidelines: ['Use unique IDs for elements to avoid conflicts, combine with atIndex() if necessary.'] - } - ] + signature: "toBeEnabled()", + description: + "Asserts that the element is enabled and can be interacted with.", + example: + 'await expect(element(by.id("submitButton"))).toBeEnabled();', + guidelines: [ + "Ensure that the element is not disabled before performing actions.", + ], }, + ], + }, + { + title: "Matchers", + items: [ { - title: 'Actions', - items: [ - { - signature: 'swipe(direction: string)', - description: 'Swipes in the specified direction.', - example: 'await swipe("up");', - guidelines: ['Use this method to scroll the screen.'] - } - ] - } - ] + signature: "by.id(id: string)", + description: "Matches elements by their ID attribute.", + example: 'element(by.id("uniqueId"))', + guidelines: [ + "Use unique IDs for elements to avoid conflicts, combine with atIndex() if necessary.", + ], + }, + ], + }, + { + title: "Actions", + items: [ + { + signature: "swipe(direction: string)", + description: "Swipes in the specified direction.", + example: 'await swipe("up");', + guidelines: ["Use this method to scroll the screen."], + }, + ], + }, + ], }; diff --git a/src/test-utils/SnapshotComparatorTestImages/SnapshotImageGetter.ts b/src/test-utils/SnapshotComparatorTestImages/SnapshotImageGetter.ts index 727bff5..7bd64fd 100644 --- a/src/test-utils/SnapshotComparatorTestImages/SnapshotImageGetter.ts +++ b/src/test-utils/SnapshotComparatorTestImages/SnapshotImageGetter.ts @@ -2,15 +2,10 @@ import path from "path"; type image = "baseline" | "different"; const imageFileNames: Record = { - baseline: 'baseline.png', - different: 'different.png', + baseline: "baseline.png", + different: "different.png", }; export const getSnapshotImage = (image: image) => { - return path.resolve( - __dirname, - '.', - imageFileNames[image] - ) + return path.resolve(__dirname, ".", imageFileNames[image]); }; - diff --git a/src/test-utils/cache.ts b/src/test-utils/cache.ts index 194d8fc..65f73a5 100644 --- a/src/test-utils/cache.ts +++ b/src/test-utils/cache.ts @@ -2,14 +2,18 @@ import fs from "fs"; export let mockedCacheFile: { [key: string]: any } | undefined; -export const mockCache = (data: { [key: string]: any } | undefined = undefined) => { - mockedCacheFile = data; +export const mockCache = ( + data: { [key: string]: any } | undefined = undefined, +) => { + mockedCacheFile = data; - (fs.writeFileSync as jest.Mock).mockImplementation((filePath, data) => { - mockedCacheFile = JSON.parse(data); - }); + (fs.writeFileSync as jest.Mock).mockImplementation((filePath, data) => { + mockedCacheFile = JSON.parse(data); + }); - (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockedCacheFile)); + (fs.readFileSync as jest.Mock).mockReturnValue( + JSON.stringify(mockedCacheFile), + ); - (fs.existsSync as jest.Mock).mockReturnValue(mockedCacheFile !== undefined); + (fs.existsSync as jest.Mock).mockReturnValue(mockedCacheFile !== undefined); }; diff --git a/src/types.ts b/src/types.ts index f50eaae..f3e4a26 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,84 +2,85 @@ * Interface for interacting with the Copilot. */ export interface CopilotFacade { - /** - * Initializes the Copilot with the given configuration. - * Must be called before any other Copilot methods. - * @param config The configuration for the Copilot. - * @note This method should only be called once. - */ - init: (config: Config) => void; - - /** - * Checks if the Copilot has been initialized. - * @returns True if the Copilot has been initialized, false otherwise. - */ - isInitialized: () => boolean; - - /** - * Start the Copilot instance. - * @note Must be called before each flow to ensure a clean state (the Copilot uses the operations history as part of - * its context). - */ - start: () => void; - - /** - * Finalizes the flow and optionally saves temporary cache data to the main cache. - * If `isCacheDisabled` is true, the temporary cache will not be saved. False is the default value. - * @param isCacheDisabled - * @note This must be called after the flow is complete. - */ - end: (isCacheDisabled?: boolean) => void; - - /** - * Performs a testing operation or series of testing operations in the app based on the given `steps`. - * @returns The result of the last step, if any. - * @example Tap on the login button - * @example Scroll down to the 7th item in the Events list - * @example The welcome message should be visible - * @example The welcome message text should be "Hello, world!" - * @example 'Tap on the login button', 'A login form should be visible' - */ - perform: (...steps: string[]) => Promise; - - - /** - * Preforms actions untill it reaches the goal. - * @example login with user "testuser" and password "testpassword123" - * @example enter the store of "my store" site and buy one of the products - * @example achive 2 points in the shape matching game - */ - pilot: (goal: string) => Promise; - - /** - * Extends the API catalog of the testing framework with additional APIs (categories and JS context). - * @param context The variables of the testing framework (i.e. exposes the matching function, expect, etc.). - * @param categories The categories to add to the API catalog. - * @note This can be used to add custom categories and items to the API catalog. - */ - extendAPICatalog: (categories: TestingFrameworkAPICatalogCategory[], context?: any,) => void; - + /** + * Initializes the Copilot with the given configuration. + * Must be called before any other Copilot methods. + * @param config The configuration for the Copilot. + * @note This method should only be called once. + */ + init: (config: Config) => void; + + /** + * Checks if the Copilot has been initialized. + * @returns True if the Copilot has been initialized, false otherwise. + */ + isInitialized: () => boolean; + + /** + * Start the Copilot instance. + * @note Must be called before each flow to ensure a clean state (the Copilot uses the operations history as part of + * its context). + */ + start: () => void; + + /** + * Finalizes the flow and optionally saves temporary cache data to the main cache. + * If `isCacheDisabled` is true, the temporary cache will not be saved. False is the default value. + * @param isCacheDisabled + * @note This must be called after the flow is complete. + */ + end: (isCacheDisabled?: boolean) => void; + + /** + * Performs a testing operation or series of testing operations in the app based on the given `steps`. + * @returns The result of the last step, if any. + * @example Tap on the login button + * @example Scroll down to the 7th item in the Events list + * @example The welcome message should be visible + * @example The welcome message text should be "Hello, world!" + * @example 'Tap on the login button', 'A login form should be visible' + */ + perform: (...steps: string[]) => Promise; + + /** + * Preforms actions untill it reaches the goal. + * @example login with user "testuser" and password "testpassword123" + * @example enter the store of "my store" site and buy one of the products + * @example achive 2 points in the shape matching game + */ + pilot: (goal: string) => Promise; + + /** + * Extends the API catalog of the testing framework with additional APIs (categories and JS context). + * @param context The variables of the testing framework (i.e. exposes the matching function, expect, etc.). + * @param categories The categories to add to the API catalog. + * @note This can be used to add custom categories and items to the API catalog. + */ + extendAPICatalog: ( + categories: TestingFrameworkAPICatalogCategory[], + context?: any, + ) => void; } /** * Interface for the testing driver that will be used to interact with the underlying testing framework. */ export interface TestingFrameworkDriver { - /** - * Takes a snapshot of the current screen and returns the path to the saved image. - * If the driver does not support image, return undefined. - */ - captureSnapshotImage: () => Promise; - - /** - * Returns the current view hierarchy in a string representation. - */ - captureViewHierarchyString: () => Promise; - - /** - * The available API methods of the testing framework. - */ - apiCatalog: TestingFrameworkAPICatalog; + /** + * Takes a snapshot of the current screen and returns the path to the saved image. + * If the driver does not support image, return undefined. + */ + captureSnapshotImage: () => Promise; + + /** + * Returns the current view hierarchy in a string representation. + */ + captureViewHierarchyString: () => Promise; + + /** + * The available API methods of the testing framework. + */ + apiCatalog: TestingFrameworkAPICatalog; } /** @@ -90,11 +91,11 @@ export interface TestingFrameworkDriver { * @property categories The available categories of the testing framework API. */ export type TestingFrameworkAPICatalog = { - name?: string; - description?: string; - context: any; - categories: TestingFrameworkAPICatalogCategory[]; -} + name?: string; + description?: string; + context: any; + categories: TestingFrameworkAPICatalogCategory[]; +}; /** * Represents a category of the API of the testing framework that can be used by Copilot. @@ -102,9 +103,9 @@ export type TestingFrameworkAPICatalog = { * @property items The items in the category. */ export type TestingFrameworkAPICatalogCategory = { - title: string; - items: TestingFrameworkAPICatalogItem[]; -} + title: string; + items: TestingFrameworkAPICatalogItem[]; +}; /** * Represents a method docs in the API of the testing framework that can be used by Copilot. @@ -125,28 +126,28 @@ export type TestingFrameworkAPICatalogCategory = { * }; */ export type TestingFrameworkAPICatalogItem = { - signature: string; - description: string; - example: string; - guidelines?: string[]; -} + signature: string; + description: string; + example: string; + guidelines?: string[]; +}; /** * Interface for the prompt handler that will be used to interact with the AI service (e.g. OpenAI). */ export interface PromptHandler { - /** - * Sends a prompt to the AI service and returns the response. - * @param prompt The prompt to send to the AI service. - * @param image Optional path to the image to upload to the AI service that captures the current UI state. - * @returns The response from the AI service. - */ - runPrompt: (prompt: string, image?: string) => Promise; - - /** - * Checks if the AI service supports snapshot images for context. - */ - isSnapshotImageSupported: () => boolean; + /** + * Sends a prompt to the AI service and returns the response. + * @param prompt The prompt to send to the AI service. + * @param image Optional path to the image to upload to the AI service that captures the current UI state. + * @returns The response from the AI service. + */ + runPrompt: (prompt: string, image?: string) => Promise; + + /** + * Checks if the AI service supports snapshot images for context. + */ + isSnapshotImageSupported: () => boolean; } /** @@ -156,30 +157,30 @@ export interface PromptHandler { * - 'disabled': No caching is used * @default 'full' */ -export type CacheMode = 'full' | 'lightweight' | 'disabled'; +export type CacheMode = "full" | "lightweight" | "disabled"; /** * The analysis mode for the Copilot. * - 'fast': Skip API search and view hierarchy analysis preprocessing (default) * - 'full': Perform complete analysis including API search and view hierarchy preprocessing * @default 'fast' */ -export type AnalysisMode = 'fast' | 'full'; +export type AnalysisMode = "fast" | "full"; /** * Configuration options for the Copilot behavior. */ export interface CopilotOptions { - /** - * The cache mode to use. - * @default 'full' - */ - cacheMode?: CacheMode; - - /** - * The analysis mode to use. - * @default 'fast' - */ - analysisMode?: AnalysisMode; + /** + * The cache mode to use. + * @default 'full' + */ + cacheMode?: CacheMode; + + /** + * The analysis mode to use. + * @default 'fast' + */ + analysisMode?: AnalysisMode; } /** * Configuration options for Copilot. @@ -188,20 +189,20 @@ export interface CopilotOptions { * @property options Additional options for configuring Copilot behavior */ export interface Config { - /** - * The testing driver to use for interacting with the underlying testing framework. - */ - frameworkDriver: TestingFrameworkDriver; - - /** - * The prompt handler to use for interacting with the AI service - */ - promptHandler: PromptHandler; - - /** - * Additional options for configuring Copilot behavior - */ - options?: CopilotOptions; + /** + * The testing driver to use for interacting with the underlying testing framework. + */ + frameworkDriver: TestingFrameworkDriver; + + /** + * The prompt handler to use for interacting with the AI service + */ + promptHandler: PromptHandler; + + /** + * Additional options for configuring Copilot behavior + */ + options?: CopilotOptions; } /** @@ -212,10 +213,10 @@ export interface Config { * @property result The result of the step. */ export type PreviousStep = { - step: string; - code: string; - result: any; -} + step: string; + code: string; + result: any; +}; /** * Represents the result of a code evaluation operation. @@ -224,55 +225,57 @@ export type PreviousStep = { * @property sharedContext The shared context that can be used in the next iteration. */ export type CodeEvaluationResult = { - code: string; - result: any; - sharedContext?: Record; -} + code: string; + result: any; + sharedContext?: Record; +}; /** * The different types of reviews pilot can perform */ -export type PilotReviewSectionType = 'ux' | 'a11y'; +export type PilotReviewSectionType = "ux" | "a11y"; /** * Represents the pilot's review object which contatins different review and other fields */ -export type PilotReview = { [key in PilotReviewSectionType]?: PilotReviewSection; }; +export type PilotReview = { + [key in PilotReviewSectionType]?: PilotReviewSection; +}; /** * Represents the output of each iteration of pilot's perform. - * @property contains the action and thoughts that were taken by the LLM + * @property contains the action and thoughts that were taken by the LLM * @property pilot's reviews for the different kind of reviews the user ask * @property the code that were created by the LLM from pilot's action */ export type PilotStepReport = { - plan: PilotStepPlan; - review?: PilotReview; - code?: string; -} + plan: PilotStepPlan; + review?: PilotReview; + code?: string; +}; /** * Represents the output of pilot. - * @property the goal pilot should achieve - * @property summary of the given steps + * @property the goal pilot should achieve + * @property summary of the given steps * @property steps report of pilot's actions (thoughts, actions, code ....) * @property pilot's reviews for the different kind of reviews the user ask */ -export type PilotReport = { - goal : string; - summary ? : string; - steps : PilotStepReport[]; - review?: PilotReview; -} +export type PilotReport = { + goal: string; + summary?: string; + steps: PilotStepReport[]; + review?: PilotReview; +}; /** * Represents the output of pilots createStepPlan method. * @property report of pilot's actions (thoughts, actions, ect ....) */ export type PilotStepPlan = { - action: string; - thoughts: string; -} + action: string; + thoughts: string; +}; /** * Represents the output of screen capturer createStepPlan method. @@ -281,10 +284,10 @@ export type PilotStepPlan = { * @property boolean indicating if snapshot is supported or not */ export type ScreenCapturerResult = { - snapshot: string | undefined; - viewHierarchy: string; - isSnapshotImageAttached: boolean; -} + snapshot: string | undefined; + viewHierarchy: string; + isSnapshotImageAttached: boolean; +}; /** * Pilots review of the currnet screen @@ -293,10 +296,10 @@ export type ScreenCapturerResult = { * @property score from 1-10 about the ux or accessability of the current step */ export type PilotReviewSection = { - summary: string; - findings? : string[]; - score: string; -} + summary: string; + findings?: string[]; + score: string; +}; /** * Represents a previous step of pilot. @@ -304,9 +307,9 @@ export type PilotReviewSection = { * @property pilot's reviews for the different kind of reviews the user ask */ export type PilotPreviousStep = { - step: string; - review?: PilotReview; -} + step: string; + review?: PilotReview; +}; /** * Represents the types of hashing algorithms that are used for snapshot hashing. @@ -323,10 +326,10 @@ export type SnapshotHashObject = Record; * if multiple values are stored in the cache for the same key, they are stored as an array of SingleCacheValue */ export type SingleCacheValue = { - snapshotHash?: SnapshotHashObject; - viewHierarchy?: string; - code: string; -} + snapshotHash?: SnapshotHashObject; + viewHierarchy?: string; + code: string; +}; /** * Represents a single cache value for the Copilot cache. @@ -337,27 +340,31 @@ export type CacheValues = SingleCacheValue[]; * Represents a list of methods to implements different hashing algorithms. */ export interface SnapshotHashing { - /** - * Hashes the given snapshot. - * @param snapshot The snapshot to hash. - * @returns The hash of the snapshot. - */ - hashSnapshot(snapshot: any): Promise; - - /** - * Calculates the distance between two snapshots. - * @param hash1 The hash of the first snapshot. - * @param hash2 The hash of the second snapshot. - * @returns The distance between the two snapshots. - */ - calculateSnapshotDistance(hash1: string, hash2: string): number; - - /** - * Checks if two snapshots are similar based on a threshold. - * @param hash1 The hash of the first snapshot. - * @param hash2 The hash of the second snapshot. - * @param threshold The threshold for similarity. - * @returns True if the snapshots are similar, false otherwise. - */ - areSnapshotsSimilar(hash1: string, hash2: string, threshold?: number): boolean; + /** + * Hashes the given snapshot. + * @param snapshot The snapshot to hash. + * @returns The hash of the snapshot. + */ + hashSnapshot(snapshot: any): Promise; + + /** + * Calculates the distance between two snapshots. + * @param hash1 The hash of the first snapshot. + * @param hash2 The hash of the second snapshot. + * @returns The distance between the two snapshots. + */ + calculateSnapshotDistance(hash1: string, hash2: string): number; + + /** + * Checks if two snapshots are similar based on a threshold. + * @param hash1 The hash of the first snapshot. + * @param hash2 The hash of the second snapshot. + * @param threshold The threshold for similarity. + * @returns True if the snapshots are similar, false otherwise. + */ + areSnapshotsSimilar( + hash1: string, + hash2: string, + threshold?: number, + ): boolean; } diff --git a/src/utils/APIFormatter.test.ts b/src/utils/APIFormatter.test.ts index 3c43e4d..c32ad83 100644 --- a/src/utils/APIFormatter.test.ts +++ b/src/utils/APIFormatter.test.ts @@ -1,101 +1,103 @@ -import {APIFormatter} from '@/utils/APIFormatter'; -import {TestingFrameworkAPICatalog} from '@/types'; +import { APIFormatter } from "@/utils/APIFormatter"; +import { TestingFrameworkAPICatalog } from "@/types"; -describe('APIFormatter', () => { - const mockAPI: TestingFrameworkAPICatalog = { - context: {}, - name: 'Test Framework', - description: 'A testing framework for testing purposes', - categories: [ - { - title: 'Actions', - items: [ - { - signature: 'tap(element: Element)', - description: 'Taps on the specified element.', - example: 'await element(by.id("button")).tap();', - guidelines: ['Ensure the element is tappable before using this method.'] - }, - { - signature: 'typeText(element: Element, text: string)', - description: 'Types the specified text into the element.', - example: 'await element(by.id("input")).typeText("Hello, World!");', - guidelines: ['Use this method only on text input elements.'] - } - ] - }, - { - title: 'Assertions', - items: [ - { - signature: 'toBeVisible()', - description: 'Asserts that the element is visible on the screen.', - example: 'await expect(element(by.id("title"))).toBeVisible();', - guidelines: ['Consider scroll position when using this assertion.'] - } - ] - }, - { - title: 'Matchers', - items: [ - { - signature: 'by.id(id: string)', - description: 'Matches elements by their ID attribute.', - example: 'element(by.id("uniqueId"))', - guidelines: ['Use unique IDs for elements to avoid conflicts.'] - } - ] - } - ] - }; +describe("APIFormatter", () => { + const mockAPI: TestingFrameworkAPICatalog = { + context: {}, + name: "Test Framework", + description: "A testing framework for testing purposes", + categories: [ + { + title: "Actions", + items: [ + { + signature: "tap(element: Element)", + description: "Taps on the specified element.", + example: 'await element(by.id("button")).tap();', + guidelines: [ + "Ensure the element is tappable before using this method.", + ], + }, + { + signature: "typeText(element: Element, text: string)", + description: "Types the specified text into the element.", + example: 'await element(by.id("input")).typeText("Hello, World!");', + guidelines: ["Use this method only on text input elements."], + }, + ], + }, + { + title: "Assertions", + items: [ + { + signature: "toBeVisible()", + description: "Asserts that the element is visible on the screen.", + example: 'await expect(element(by.id("title"))).toBeVisible();', + guidelines: ["Consider scroll position when using this assertion."], + }, + ], + }, + { + title: "Matchers", + items: [ + { + signature: "by.id(id: string)", + description: "Matches elements by their ID attribute.", + example: 'element(by.id("uniqueId"))', + guidelines: ["Use unique IDs for elements to avoid conflicts."], + }, + ], + }, + ], + }; - let apiFormatter: APIFormatter; + let apiFormatter: APIFormatter; - beforeEach(() => { - apiFormatter = new APIFormatter(mockAPI); - }); + beforeEach(() => { + apiFormatter = new APIFormatter(mockAPI); + }); - describe('formatAPIMethod', () => { - it('should format method with guidelines', () => { - const method = mockAPI.categories[0].items[0]; - expect(apiFormatter.formatAPIMethod(method)).toMatchSnapshot(); - }); + describe("formatAPIMethod", () => { + it("should format method with guidelines", () => { + const method = mockAPI.categories[0].items[0]; + expect(apiFormatter.formatAPIMethod(method)).toMatchSnapshot(); + }); - it('should format method without guidelines', () => { - const method = { - ...mockAPI.categories[0].items[0], - guidelines: undefined - }; - expect(apiFormatter.formatAPIMethod(method)).toMatchSnapshot(); - }); + it("should format method without guidelines", () => { + const method = { + ...mockAPI.categories[0].items[0], + guidelines: undefined, + }; + expect(apiFormatter.formatAPIMethod(method)).toMatchSnapshot(); }); + }); - describe('formatAPICategory', () => { - it('should format category with all its methods', () => { - const category = mockAPI.categories[0]; - expect(apiFormatter.formatAPICategory(category)).toMatchSnapshot(); - }); + describe("formatAPICategory", () => { + it("should format category with all its methods", () => { + const category = mockAPI.categories[0]; + expect(apiFormatter.formatAPICategory(category)).toMatchSnapshot(); + }); - it('should format empty category', () => { - const category = { - title: 'Empty Category', - items: [] - }; - expect(apiFormatter.formatAPICategory(category)).toMatchSnapshot(); - }); + it("should format empty category", () => { + const category = { + title: "Empty Category", + items: [], + }; + expect(apiFormatter.formatAPICategory(category)).toMatchSnapshot(); }); + }); - describe('formatAPIInfo', () => { - it('should format all methods grouped by categories', () => { - expect(apiFormatter.formatAPIInfo()).toMatchSnapshot(); - }); + describe("formatAPIInfo", () => { + it("should format all methods grouped by categories", () => { + expect(apiFormatter.formatAPIInfo()).toMatchSnapshot(); + }); - it('should format empty API catalog', () => { - const emptyFormatter = new APIFormatter({ - context: {}, - categories: [] - }); - expect(emptyFormatter.formatAPIInfo()).toMatchSnapshot(); - }); + it("should format empty API catalog", () => { + const emptyFormatter = new APIFormatter({ + context: {}, + categories: [], + }); + expect(emptyFormatter.formatAPIInfo()).toMatchSnapshot(); }); + }); }); diff --git a/src/utils/APIFormatter.ts b/src/utils/APIFormatter.ts index e895783..ef73929 100644 --- a/src/utils/APIFormatter.ts +++ b/src/utils/APIFormatter.ts @@ -1,58 +1,58 @@ import { - TestingFrameworkAPICatalog, - TestingFrameworkAPICatalogCategory, - TestingFrameworkAPICatalogItem -} from '@/types'; + TestingFrameworkAPICatalog, + TestingFrameworkAPICatalogCategory, + TestingFrameworkAPICatalogItem, +} from "@/types"; export class APIFormatter { - constructor(private apiCatalog: TestingFrameworkAPICatalog) {} + constructor(private apiCatalog: TestingFrameworkAPICatalog) {} - /** - * Formats API method - */ - formatAPIMethod(method: TestingFrameworkAPICatalogItem): string { - const methodInfo = [ - `#### ${method.signature}`, - "", - method.description, - "", - "##### Example", - "", - "```", - method.example, - "```", - "" - ]; + /** + * Formats API method + */ + formatAPIMethod(method: TestingFrameworkAPICatalogItem): string { + const methodInfo = [ + `#### ${method.signature}`, + "", + method.description, + "", + "##### Example", + "", + "```", + method.example, + "```", + "", + ]; - if (method.guidelines && method.guidelines.length > 0) { - methodInfo.push( - "##### Guidelines", - "", - ...method.guidelines.map(g => `- ${g}`), - "" - ); - } - - return methodInfo.join('\n'); + if (method.guidelines && method.guidelines.length > 0) { + methodInfo.push( + "##### Guidelines", + "", + ...method.guidelines.map((g) => `- ${g}`), + "", + ); } - /** - * Formats API category with its methods - */ - formatAPICategory(category: TestingFrameworkAPICatalogCategory): string { - return [ - `### ${category.title}`, - "", - ...category.items.map(item => this.formatAPIMethod(item)) - ].join('\n'); - } + return methodInfo.join("\n"); + } - /** - * Formats all API methods grouped by categories - */ - formatAPIInfo(): string { - return this.apiCatalog.categories - .map(category => this.formatAPICategory(category)) - .join('\n'); - } + /** + * Formats API category with its methods + */ + formatAPICategory(category: TestingFrameworkAPICatalogCategory): string { + return [ + `### ${category.title}`, + "", + ...category.items.map((item) => this.formatAPIMethod(item)), + ].join("\n"); + } + + /** + * Formats all API methods grouped by categories + */ + formatAPIInfo(): string { + return this.apiCatalog.categories + .map((category) => this.formatAPICategory(category)) + .join("\n"); + } } diff --git a/src/utils/CacheHandler.test.ts b/src/utils/CacheHandler.test.ts index 252de66..3dedf4f 100644 --- a/src/utils/CacheHandler.test.ts +++ b/src/utils/CacheHandler.test.ts @@ -1,102 +1,107 @@ -import {CacheHandler} from './CacheHandler'; -import {mockCache, mockedCacheFile} from "../test-utils/cache"; +import { CacheHandler } from "./CacheHandler"; +import { mockCache, mockedCacheFile } from "../test-utils/cache"; -jest.mock('fs'); +jest.mock("fs"); -describe('CacheHandler', () => { - let cacheHandler: CacheHandler; +describe("CacheHandler", () => { + let cacheHandler: CacheHandler; - beforeEach(() => { - jest.resetAllMocks(); - cacheHandler = new CacheHandler(); - }); + beforeEach(() => { + jest.resetAllMocks(); + cacheHandler = new CacheHandler(); + }); - describe('cache and file operations', () => { - it('should load cache from file successfully if the file exists and is valid', () => { - mockCache({'cacheKey': 'value'}); + describe("cache and file operations", () => { + it("should load cache from file successfully if the file exists and is valid", () => { + mockCache({ cacheKey: "value" }); - expect(cacheHandler.getStepFromCache('cacheKey')).toBeUndefined(); + expect(cacheHandler.getStepFromCache("cacheKey")).toBeUndefined(); - cacheHandler.loadCacheFromFile(); + cacheHandler.loadCacheFromFile(); - expect(cacheHandler.getStepFromCache('cacheKey')).toBe('value'); - }); + expect(cacheHandler.getStepFromCache("cacheKey")).toBe("value"); + }); - it('should save cache to file successfully', () => { - mockCache(); + it("should save cache to file successfully", () => { + mockCache(); - cacheHandler.addToTemporaryCache('cacheKey', 'value'); - cacheHandler.flushTemporaryCache(); + cacheHandler.addToTemporaryCache("cacheKey", "value"); + cacheHandler.flushTemporaryCache(); - expect(mockedCacheFile).toEqual({'cacheKey': ['value']}); - }); + expect(mockedCacheFile).toEqual({ cacheKey: ["value"] }); }); + }); - describe('addToTemporaryCache', () => { - it('should not save to cache', () => { - mockCache(); + describe("addToTemporaryCache", () => { + it("should not save to cache", () => { + mockCache(); - cacheHandler.addToTemporaryCache('cacheKey', 'value'); + cacheHandler.addToTemporaryCache("cacheKey", "value"); - expect(cacheHandler.getStepFromCache('cacheKey')).toBeUndefined(); - expect(mockedCacheFile).toBeUndefined(); - }); + expect(cacheHandler.getStepFromCache("cacheKey")).toBeUndefined(); + expect(mockedCacheFile).toBeUndefined(); }); + }); - describe('getStepFromCache', () => { - it('should retrieve a value from cache using getStepFromCache', () => { - cacheHandler.addToTemporaryCache('some_key', 'value'); - cacheHandler.flushTemporaryCache(); + describe("getStepFromCache", () => { + it("should retrieve a value from cache using getStepFromCache", () => { + cacheHandler.addToTemporaryCache("some_key", "value"); + cacheHandler.flushTemporaryCache(); - const result = cacheHandler.getStepFromCache('some_key'); + const result = cacheHandler.getStepFromCache("some_key"); - expect(result).toEqual(['value']); - }); + expect(result).toEqual(["value"]); + }); - it('should return undefined if the key does not exist in cache', () => { - const result = cacheHandler.getStepFromCache('non_existent_key'); + it("should return undefined if the key does not exist in cache", () => { + const result = cacheHandler.getStepFromCache("non_existent_key"); - expect(result).toBeUndefined(); - }); + expect(result).toBeUndefined(); }); + }); - describe('flushTemporaryCache', () => { - it('should move all temporary cache entries to the main cache', () => { - expect(cacheHandler.getStepFromCache('cacheKey1')).toBeUndefined(); + describe("flushTemporaryCache", () => { + it("should move all temporary cache entries to the main cache", () => { + expect(cacheHandler.getStepFromCache("cacheKey1")).toBeUndefined(); - cacheHandler.addToTemporaryCache('cacheKey1', 'value1'); - cacheHandler.addToTemporaryCache('cacheKey2', 'value2'); - cacheHandler.addToTemporaryCache('cacheKey3', 'value3'); + cacheHandler.addToTemporaryCache("cacheKey1", "value1"); + cacheHandler.addToTemporaryCache("cacheKey2", "value2"); + cacheHandler.addToTemporaryCache("cacheKey3", "value3"); - cacheHandler.flushTemporaryCache() + cacheHandler.flushTemporaryCache(); - expect(cacheHandler.getStepFromCache('cacheKey1')).toEqual(['value1']); - expect(cacheHandler.getStepFromCache('cacheKey3')).toEqual(['value3']); - expect(cacheHandler.getStepFromCache('cacheKey2')).not.toEqual(['value3']); - }); + expect(cacheHandler.getStepFromCache("cacheKey1")).toEqual(["value1"]); + expect(cacheHandler.getStepFromCache("cacheKey3")).toEqual(["value3"]); + expect(cacheHandler.getStepFromCache("cacheKey2")).not.toEqual([ + "value3", + ]); + }); - it('should get the updated value from cache', () => { - expect(cacheHandler.getStepFromCache('cacheKey1')).toBeUndefined(); + it("should get the updated value from cache", () => { + expect(cacheHandler.getStepFromCache("cacheKey1")).toBeUndefined(); - cacheHandler.addToTemporaryCache('cacheKey1', 'value1'); - cacheHandler.addToTemporaryCache('cacheKey1', 'value2'); + cacheHandler.addToTemporaryCache("cacheKey1", "value1"); + cacheHandler.addToTemporaryCache("cacheKey1", "value2"); - cacheHandler.flushTemporaryCache() + cacheHandler.flushTemporaryCache(); - expect(cacheHandler.getStepFromCache('cacheKey1')).toEqual(['value1', 'value2']); - }); + expect(cacheHandler.getStepFromCache("cacheKey1")).toEqual([ + "value1", + "value2", + ]); }); + }); - it('should clear the temporary cache', () => { - mockCache(); - cacheHandler.addToTemporaryCache('cacheKey', 'value'); + it("should clear the temporary cache", () => { + mockCache(); + cacheHandler.addToTemporaryCache("cacheKey", "value"); - expect(cacheHandler.getStepFromCache('cacheKey')).toBeUndefined(); + expect(cacheHandler.getStepFromCache("cacheKey")).toBeUndefined(); - cacheHandler.clearTemporaryCache(); - cacheHandler.flushTemporaryCache() + cacheHandler.clearTemporaryCache(); + cacheHandler.flushTemporaryCache(); - expect(cacheHandler.getStepFromCache('cacheKey')).toBeUndefined(); - expect(mockedCacheFile).toStrictEqual({}); - }); + expect(cacheHandler.getStepFromCache("cacheKey")).toBeUndefined(); + expect(mockedCacheFile).toStrictEqual({}); + }); }); diff --git a/src/utils/CacheHandler.ts b/src/utils/CacheHandler.ts index 3823ae1..2e75f92 100644 --- a/src/utils/CacheHandler.ts +++ b/src/utils/CacheHandler.ts @@ -1,57 +1,61 @@ -import fs from 'fs'; -import path from 'path'; +import fs from "fs"; +import path from "path"; export class CacheHandler { - private cache: Map = new Map(); - private temporaryCache: Map = new Map(); - private readonly cacheFilePath: string; - - constructor(cacheFileName: string = 'detox_copilot_cache.json') { - this.cacheFilePath = path.resolve(process.cwd(), cacheFileName); - } - - public loadCacheFromFile(): void { - try { - if (fs.existsSync(this.cacheFilePath)) { - const readFileSync = fs.readFileSync; - const data = fs.readFileSync(this.cacheFilePath, 'utf-8'); - const json = JSON.parse(data); - this.cache = new Map(Object.entries(json)); - } else { - this.cache.clear(); // Ensure cache is empty if file doesn't exist - } - } catch (error) { - console.warn('Error loading cache from file:', error); - this.cache.clear(); // Clear cache on error to avoid stale data - } - } - - private saveCacheToFile(): void { - try { - const json = Object.fromEntries(this.cache); - fs.writeFileSync(this.cacheFilePath, JSON.stringify(json, null, 2), { flag: 'w+' }); - } catch (error) { - console.error('Error saving cache to file:', error); - } - } - - public getStepFromCache(key: string): any | undefined { - return this.cache.get(key); - } - - public addToTemporaryCache(key: string, value: any): void { - this.temporaryCache.set(key, [...this.temporaryCache.get(key) ?? [], value]); + private cache: Map = new Map(); + private temporaryCache: Map = new Map(); + private readonly cacheFilePath: string; + + constructor(cacheFileName: string = "detox_copilot_cache.json") { + this.cacheFilePath = path.resolve(process.cwd(), cacheFileName); + } + + public loadCacheFromFile(): void { + try { + if (fs.existsSync(this.cacheFilePath)) { + const data = fs.readFileSync(this.cacheFilePath, "utf-8"); + const json = JSON.parse(data); + this.cache = new Map(Object.entries(json)); + } else { + this.cache.clear(); // Ensure cache is empty if file doesn't exist + } + } catch (error) { + console.warn("Error loading cache from file:", error); + this.cache.clear(); // Clear cache on error to avoid stale data } - - public flushTemporaryCache(): void { - this.temporaryCache.forEach((value, key) => { - this.cache.set(key, value); - }); - this.saveCacheToFile(); - this.clearTemporaryCache(); - } - - public clearTemporaryCache(): void { - this.temporaryCache.clear(); + } + + private saveCacheToFile(): void { + try { + const json = Object.fromEntries(this.cache); + fs.writeFileSync(this.cacheFilePath, JSON.stringify(json, null, 2), { + flag: "w+", + }); + } catch (error) { + console.error("Error saving cache to file:", error); } + } + + public getStepFromCache(key: string): any | undefined { + return this.cache.get(key); + } + + public addToTemporaryCache(key: string, value: any): void { + this.temporaryCache.set(key, [ + ...(this.temporaryCache.get(key) ?? []), + value, + ]); + } + + public flushTemporaryCache(): void { + this.temporaryCache.forEach((value, key) => { + this.cache.set(key, value); + }); + this.saveCacheToFile(); + this.clearTemporaryCache(); + } + + public clearTemporaryCache(): void { + this.temporaryCache.clear(); + } } diff --git a/src/utils/CodeEvaluator.test.ts b/src/utils/CodeEvaluator.test.ts index d246b9c..fe346b8 100644 --- a/src/utils/CodeEvaluator.test.ts +++ b/src/utils/CodeEvaluator.test.ts @@ -1,133 +1,149 @@ -import {CodeEvaluator} from '@/utils/CodeEvaluator'; - -describe('CodeEvaluator', () => { - let codeEvaluator: CodeEvaluator; - - beforeEach(() => { - codeEvaluator = new CodeEvaluator(); +import { CodeEvaluator } from "@/utils/CodeEvaluator"; + +describe("CodeEvaluator", () => { + let codeEvaluator: CodeEvaluator; + + beforeEach(() => { + codeEvaluator = new CodeEvaluator(); + }); + + it("should evaluate valid code successfully", async () => { + const validCode = "return 2 + 2;"; + await expect(codeEvaluator.evaluate(validCode, {})).resolves.not.toThrow(); + }); + + it("should evaluate valid code with comments successfully", async () => { + const validCode = "return 2 + 2; // This is a comment"; + await expect(codeEvaluator.evaluate(validCode, {})).resolves.not.toThrow(); + }); + + it("should evaluate valid code with multiple lines successfully", async () => { + const validCode = "return 2 + 2;\nreturn 3 + 3;"; + await expect(codeEvaluator.evaluate(validCode, {})).resolves.not.toThrow(); + }); + + it("should evaluate valid code with context successfully", async () => { + const contextVariable = 43; + const validCode = "return contextVariable - 1;"; + + await expect( + codeEvaluator.evaluate(validCode, { contextVariable }), + ).resolves.toStrictEqual({ + code: "return contextVariable - 1;", + result: 42, + sharedContext: {}, }); - - it('should evaluate valid code successfully', async () => { - const validCode = 'return 2 + 2;'; - await expect(codeEvaluator.evaluate(validCode, {})).resolves.not.toThrow(); + }); + + it("should throw CodeEvaluationError for invalid code", async () => { + const invalidCode = 'throw new Error("Test error");'; + await expect(codeEvaluator.evaluate(invalidCode, {})).rejects.toThrow( + new Error("Test error"), + ); + }); + + it("should handle asynchronous code", async () => { + const asyncCode = + 'await new Promise(resolve => setTimeout(resolve, 100)); return "done";'; + + await expect(codeEvaluator.evaluate(asyncCode, {})).resolves.toStrictEqual({ + code: 'await new Promise(resolve => setTimeout(resolve, 100)); return "done";', + result: "done", + sharedContext: {}, }); - - it('should evaluate valid code with comments successfully', async () => { - const validCode = 'return 2 + 2; // This is a comment'; - await expect(codeEvaluator.evaluate(validCode, {})).resolves.not.toThrow(); + }); + + it("should throw CodeEvaluationError with original error message", async () => { + const errorCode = 'throw new Error("Custom error message");'; + await expect(codeEvaluator.evaluate(errorCode, {})).rejects.toThrow( + new Error("Custom error message"), + ); + }); + + describe("shared context", () => { + it("should allow reading from shared context", async () => { + const sharedContext = { value: 42 }; + const code = "return sharedContext.value;"; + + const result = await codeEvaluator.evaluate(code, {}, sharedContext); + + expect(result).toStrictEqual({ + code, + result: 42, + sharedContext: { value: 42 }, + }); }); - it('should evaluate valid code with multiple lines successfully', async () => { - const validCode = 'return 2 + 2;\nreturn 3 + 3;'; - await expect(codeEvaluator.evaluate(validCode, {})).resolves.not.toThrow(); - }); + it("should allow writing to shared context", async () => { + const sharedContext = { value: 42 }; + const code = + "sharedContext.newValue = sharedContext.value * 2; return true;"; - it('should evaluate valid code with context successfully', async () => { - const contextVariable = 43; - const validCode = 'return contextVariable - 1;'; + const result = await codeEvaluator.evaluate(code, {}, sharedContext); - await expect(codeEvaluator.evaluate(validCode, {contextVariable})).resolves.toStrictEqual({ - code: 'return contextVariable - 1;', - result: 42, - sharedContext: {} - }); + expect(result).toStrictEqual({ + code, + result: true, + sharedContext: { value: 42, newValue: 84 }, + }); }); - it('should throw CodeEvaluationError for invalid code', async () => { - const invalidCode = 'throw new Error("Test error");'; - await expect(codeEvaluator.evaluate(invalidCode, {})).rejects.toThrow(new Error('Test error')); + it("should preserve shared context between evaluations", async () => { + const sharedContext = {}; + + // First evaluation stores a value + await codeEvaluator.evaluate( + "sharedContext.value = 42;", + {}, + sharedContext, + ); + + // Second evaluation uses the stored value + const result = await codeEvaluator.evaluate( + "return sharedContext.value;", + {}, + sharedContext, + ); + + expect(result).toStrictEqual({ + code: "return sharedContext.value;", + result: 42, + sharedContext: { value: 42 }, + }); }); - it('should handle asynchronous code', async () => { - const asyncCode = 'await new Promise(resolve => setTimeout(resolve, 100)); return "done";'; + it("should handle undefined shared context", async () => { + const code = "return 42;"; - await expect(codeEvaluator.evaluate(asyncCode, {})).resolves.toStrictEqual({ - code: 'await new Promise(resolve => setTimeout(resolve, 100)); return "done";', - result: 'done', - sharedContext: {} - }); - }); + const result = await codeEvaluator.evaluate(code, {}); - it('should throw CodeEvaluationError with original error message', async () => { - const errorCode = 'throw new Error("Custom error message");'; - await expect(codeEvaluator.evaluate(errorCode, {})).rejects.toThrow(new Error('Custom error message')); + expect(result).toStrictEqual({ + code, + result: 42, + sharedContext: {}, + }); }); - describe('shared context', () => { - it('should allow reading from shared context', async () => { - const sharedContext = { value: 42 }; - const code = 'return sharedContext.value;'; - - const result = await codeEvaluator.evaluate(code, {}, sharedContext); - - expect(result).toStrictEqual({ - code, - result: 42, - sharedContext: { value: 42 } - }); - }); - - it('should allow writing to shared context', async () => { - const sharedContext = { value: 42 }; - const code = 'sharedContext.newValue = sharedContext.value * 2; return true;'; - - const result = await codeEvaluator.evaluate(code, {}, sharedContext); - - expect(result).toStrictEqual({ - code, - result: true, - sharedContext: { value: 42, newValue: 84 } - }); - }); - - it('should preserve shared context between evaluations', async () => { - const sharedContext = {}; - - // First evaluation stores a value - await codeEvaluator.evaluate('sharedContext.value = 42;', {}, sharedContext); - - // Second evaluation uses the stored value - const result = await codeEvaluator.evaluate('return sharedContext.value;', {}, sharedContext); - - expect(result).toStrictEqual({ - code: 'return sharedContext.value;', - result: 42, - sharedContext: { value: 42 } - }); - }); - - it('should handle undefined shared context', async () => { - const code = 'return 42;'; - - const result = await codeEvaluator.evaluate(code, {}); - - expect(result).toStrictEqual({ - code, - result: 42, - sharedContext: {} - }); - }); - - it('should handle complex shared context operations', async () => { - const sharedContext = { users: [] }; - const code = ` + it("should handle complex shared context operations", async () => { + const sharedContext = { users: [] }; + const code = ` sharedContext.users.push({ id: 1, name: 'Alice' }); sharedContext.users.push({ id: 2, name: 'Bob' }); return sharedContext.users.length; `; - - const result = await codeEvaluator.evaluate(code, {}, sharedContext); - - expect(result).toStrictEqual({ - code, - result: 2, - sharedContext: { - users: [ - { id: 1, name: 'Alice' }, - { id: 2, name: 'Bob' } - ] - } - }); - }); + + const result = await codeEvaluator.evaluate(code, {}, sharedContext); + + expect(result).toStrictEqual({ + code, + result: 2, + sharedContext: { + users: [ + { id: 1, name: "Alice" }, + { id: 2, name: "Bob" }, + ], + }, + }); }); + }); }); diff --git a/src/utils/CodeEvaluator.ts b/src/utils/CodeEvaluator.ts index 8bf6fe5..c56eb30 100644 --- a/src/utils/CodeEvaluator.ts +++ b/src/utils/CodeEvaluator.ts @@ -1,30 +1,50 @@ -import { CodeEvaluationError } from '@/errors/CodeEvaluationError'; -import {CodeEvaluationResult} from "@/types"; +import { CodeEvaluationError } from "@/errors/CodeEvaluationError"; +import { CodeEvaluationResult } from "@/types"; export class CodeEvaluator { - async evaluate(code: string, context: any, sharedContext: Record = {}): Promise { - const asyncFunction = this.createAsyncFunction(code, context, sharedContext); - const result = await asyncFunction(); + async evaluate( + code: string, + context: any, + sharedContext: Record = {}, + ): Promise { + const asyncFunction = this.createAsyncFunction( + code, + context, + sharedContext, + ); + const result = await asyncFunction(); - return { code, result, sharedContext } - } + return { code, result, sharedContext }; + } - private createAsyncFunction(code: string, context: any, sharedContext: Record): Function { - // todo: this is a temp log for debugging, we'll need to pass a logging mechanism from the framework. - console.log("\x1b[90m%s\x1b[0m\x1b[92m%s\x1b[0m", "Copilot evaluating code block:\n", `${code}\n`); + private createAsyncFunction( + code: string, + context: any, + sharedContext: Record, + ): () => Promise { + // todo: this is a temp log for debugging, we'll need to pass a logging mechanism from the framework. + console.log( + "\x1b[90m%s\x1b[0m\x1b[92m%s\x1b[0m", + "Copilot evaluating code block:\n", + `${code}\n`, + ); - try { - const contextValues = Object.values(context); + try { + const contextValues = Object.values(context); - // Wrap the code in an immediately-invoked async function expression (IIFE), and inject context variables into the function - return new Function(...Object.keys(context), 'sharedContext', `return (async () => { + // Wrap the code in an immediately-invoked async function expression (IIFE), and inject context variables into the function + return new Function( + ...Object.keys(context), + "sharedContext", + `return (async () => { ${code} - })();`).bind(null, ...contextValues, sharedContext); - } catch (error) { - const underlyingErrorMessage = (error as Error)?.message; - throw new CodeEvaluationError( - `Failed to execute test step code, error: ${underlyingErrorMessage}:\n\`\`\`\n${code}\n\`\`\`` - ); - } + })();`, + ).bind(null, ...contextValues, sharedContext); + } catch (error) { + const underlyingErrorMessage = (error as Error)?.message; + throw new CodeEvaluationError( + `Failed to execute test step code, error: ${underlyingErrorMessage}:\n\`\`\`\n${code}\n\`\`\``, + ); } + } } diff --git a/src/utils/CopilotAPISearchPromptCreator.test.ts b/src/utils/CopilotAPISearchPromptCreator.test.ts index 11da966..03e4a22 100644 --- a/src/utils/CopilotAPISearchPromptCreator.test.ts +++ b/src/utils/CopilotAPISearchPromptCreator.test.ts @@ -1,105 +1,109 @@ -import {CopilotAPISearchPromptCreator} from '@/utils/CopilotAPISearchPromptCreator'; -import {TestingFrameworkAPICatalog} from '@/types'; +import { CopilotAPISearchPromptCreator } from "@/utils/CopilotAPISearchPromptCreator"; +import { TestingFrameworkAPICatalog } from "@/types"; -describe('CopilotAPISearchPromptCreator', () => { - const mockAPI: TestingFrameworkAPICatalog = { - context: {}, - name: 'Test Framework', - description: 'A testing framework for testing purposes', - categories: [ - { - title: 'Actions', - items: [ - { - signature: 'tap(element: Element)', - description: 'Taps on the specified element.', - example: 'await element(by.id("button")).tap();', - guidelines: ['Ensure the element is tappable before using this method.'] - }, - { - signature: 'typeText(element: Element, text: string)', - description: 'Types the specified text into the element.', - example: 'await element(by.id("input")).typeText("Hello, World!");', - guidelines: ['Use this method only on text input elements.'] - } - ] - }, - { - title: 'Assertions', - items: [ - { - signature: 'toBeVisible()', - description: 'Asserts that the element is visible on the screen.', - example: 'await expect(element(by.id("title"))).toBeVisible();', - guidelines: ['Consider scroll position when using this assertion.'] - } - ] - }, - { - title: 'Matchers', - items: [ - { - signature: 'by.id(id: string)', - description: 'Matches elements by their ID attribute.', - example: 'element(by.id("uniqueId"))', - guidelines: ['Use unique IDs for elements to avoid conflicts.'] - } - ] - } - ] - }; +describe("CopilotAPISearchPromptCreator", () => { + const mockAPI: TestingFrameworkAPICatalog = { + context: {}, + name: "Test Framework", + description: "A testing framework for testing purposes", + categories: [ + { + title: "Actions", + items: [ + { + signature: "tap(element: Element)", + description: "Taps on the specified element.", + example: 'await element(by.id("button")).tap();', + guidelines: [ + "Ensure the element is tappable before using this method.", + ], + }, + { + signature: "typeText(element: Element, text: string)", + description: "Types the specified text into the element.", + example: 'await element(by.id("input")).typeText("Hello, World!");', + guidelines: ["Use this method only on text input elements."], + }, + ], + }, + { + title: "Assertions", + items: [ + { + signature: "toBeVisible()", + description: "Asserts that the element is visible on the screen.", + example: 'await expect(element(by.id("title"))).toBeVisible();', + guidelines: ["Consider scroll position when using this assertion."], + }, + ], + }, + { + title: "Matchers", + items: [ + { + signature: "by.id(id: string)", + description: "Matches elements by their ID attribute.", + example: 'element(by.id("uniqueId"))', + guidelines: ["Use unique IDs for elements to avoid conflicts."], + }, + ], + }, + ], + }; - let promptCreator: CopilotAPISearchPromptCreator; + let promptCreator: CopilotAPISearchPromptCreator; - beforeEach(() => { - promptCreator = new CopilotAPISearchPromptCreator(mockAPI); - }); - - it('should create the prompt properly', () => { - const step = 'tap the login button'; - const prompt = promptCreator.createPrompt(step); + beforeEach(() => { + promptCreator = new CopilotAPISearchPromptCreator(mockAPI); + }); - expect(prompt).toMatchSnapshot(); - }); + it("should create the prompt properly", () => { + const step = "tap the login button"; + const prompt = promptCreator.createPrompt(step); - it('should include view analysis result when provided', () => { - const step = 'tap the login button'; - const viewAnalysisResult = 'Basic Element Description:\nA button in a navigation bar\n\nElement Location Analysis:\n...'; - const prompt = promptCreator.createPrompt(step, viewAnalysisResult); + expect(prompt).toMatchSnapshot(); + }); - expect(prompt).toMatchSnapshot(); - }); + it("should include view analysis result when provided", () => { + const step = "tap the login button"; + const viewAnalysisResult = + "Basic Element Description:\nA button in a navigation bar\n\nElement Location Analysis:\n..."; + const prompt = promptCreator.createPrompt(step, viewAnalysisResult); - it('should handle empty API catalog', () => { - const emptyAPICreator = new CopilotAPISearchPromptCreator({ - context: {}, - categories: [] - }); - const step = 'tap the login button'; - const prompt = emptyAPICreator.createPrompt(step); + expect(prompt).toMatchSnapshot(); + }); - expect(prompt).toMatchSnapshot(); + it("should handle empty API catalog", () => { + const emptyAPICreator = new CopilotAPISearchPromptCreator({ + context: {}, + categories: [], }); + const step = "tap the login button"; + const prompt = emptyAPICreator.createPrompt(step); - it('should handle complex multi-step intent', () => { - const step = 'scroll to the bottom of the list, find the last item with title "Complete", and verify it is visible'; - const prompt = promptCreator.createPrompt(step); + expect(prompt).toMatchSnapshot(); + }); - expect(prompt).toMatchSnapshot(); - }); + it("should handle complex multi-step intent", () => { + const step = + 'scroll to the bottom of the list, find the last item with title "Complete", and verify it is visible'; + const prompt = promptCreator.createPrompt(step); - it('should handle special characters in step', () => { - const step = 'verify text contains "Hello & goodbye" with chars'; - const prompt = promptCreator.createPrompt(step); + expect(prompt).toMatchSnapshot(); + }); - expect(prompt).toMatchSnapshot(); - }); + it("should handle special characters in step", () => { + const step = 'verify text contains "Hello & goodbye" with chars'; + const prompt = promptCreator.createPrompt(step); - it('should handle empty view analysis result', () => { - const step = 'tap the login button'; - const viewAnalysisResult = ''; - const prompt = promptCreator.createPrompt(step, viewAnalysisResult); + expect(prompt).toMatchSnapshot(); + }); - expect(prompt).toMatchSnapshot(); - }); + it("should handle empty view analysis result", () => { + const step = "tap the login button"; + const viewAnalysisResult = ""; + const prompt = promptCreator.createPrompt(step, viewAnalysisResult); + + expect(prompt).toMatchSnapshot(); + }); }); diff --git a/src/utils/CopilotAPISearchPromptCreator.ts b/src/utils/CopilotAPISearchPromptCreator.ts index 2480d5e..e95299f 100644 --- a/src/utils/CopilotAPISearchPromptCreator.ts +++ b/src/utils/CopilotAPISearchPromptCreator.ts @@ -1,78 +1,75 @@ -import {TestingFrameworkAPICatalog} from '@/types'; -import {APIFormatter} from '@/utils/APIFormatter'; +import { TestingFrameworkAPICatalog } from "@/types"; +import { APIFormatter } from "@/utils/APIFormatter"; export class CopilotAPISearchPromptCreator { - private apiFormatter: APIFormatter; + private apiFormatter: APIFormatter; - constructor(private apiCatalog: TestingFrameworkAPICatalog) { - this.apiFormatter = new APIFormatter(apiCatalog); - } + constructor(private apiCatalog: TestingFrameworkAPICatalog) { + this.apiFormatter = new APIFormatter(apiCatalog); + } - createPrompt(step: string, viewAnalysisResult?: string): string { - return [ - "# API Method Search", - "", - "## Task Description", - "", - `Find API methods and categories that semantically match this task: "${step}"`, - "", - ...(viewAnalysisResult ? [ - "## View Analysis Context", - "", - viewAnalysisResult, - "" - ] : []), - "## Search Context", - "The following results are based on semantic similarity to your task. They represent potential matches that should be carefully evaluated, not necessarily the optimal or recommended solutions.", - "", - "## Available API Methods", - "", - this.apiFormatter.formatAPIInfo(), - "", - "## Instructions", - "", - "1. Analyze the semantic patterns in the task:", - " - Key terms and actions", - " - Object and state descriptions", - " - Expected behaviors and outcomes", - "", - "2. Review semantic matches found:", - " - Evaluate similarity scores", - " - Consider partial matches", - " - Note contextual relevance", - "", - "3. For each semantic match, assess:", - " - Match confidence level", - " - Contextual applicability", - " - Potential limitations", - "", - "Please provide your response in the following format:", - "", - "```", - "Semantic Category Matches:", - "1. [Category Name]", - " - Match Confidence: [High/Medium/Low - Why this category semantically matches]", - " - Context Notes: [Important contextual considerations]", - " - Limitations: [Where the semantic match might not translate to practical use]", - "", - "Semantic API Matches:", - "1. methodName(params)", - " - Match Confidence: [High/Medium/Low - Explain the semantic similarity]", - " - Context Notes: [How the semantic match relates to actual usage]", - " - Limitations: [Potential gaps between semantic match and practical application]", - "```", - "", - "Search Evaluation:", - "- Consider that semantic matches may not be optimal solutions", - "- Evaluate practical applicability beyond semantic similarity", - "- Look for gaps between semantic matches and actual requirements", - "- Consider alternative approaches if semantic matches are weak", - "", - "Additional Context:", - "- Note the confidence level of semantic matches", - "- Highlight where semantic similarity might be misleading", - "- Suggest verification steps before accepting matches", - "", - ].join('\n'); - } + createPrompt(step: string, viewAnalysisResult?: string): string { + return [ + "# API Method Search", + "", + "## Task Description", + "", + `Find API methods and categories that semantically match this task: "${step}"`, + "", + ...(viewAnalysisResult + ? ["## View Analysis Context", "", viewAnalysisResult, ""] + : []), + "## Search Context", + "The following results are based on semantic similarity to your task. They represent potential matches that should be carefully evaluated, not necessarily the optimal or recommended solutions.", + "", + "## Available API Methods", + "", + this.apiFormatter.formatAPIInfo(), + "", + "## Instructions", + "", + "1. Analyze the semantic patterns in the task:", + " - Key terms and actions", + " - Object and state descriptions", + " - Expected behaviors and outcomes", + "", + "2. Review semantic matches found:", + " - Evaluate similarity scores", + " - Consider partial matches", + " - Note contextual relevance", + "", + "3. For each semantic match, assess:", + " - Match confidence level", + " - Contextual applicability", + " - Potential limitations", + "", + "Please provide your response in the following format:", + "", + "```", + "Semantic Category Matches:", + "1. [Category Name]", + " - Match Confidence: [High/Medium/Low - Why this category semantically matches]", + " - Context Notes: [Important contextual considerations]", + " - Limitations: [Where the semantic match might not translate to practical use]", + "", + "Semantic API Matches:", + "1. methodName(params)", + " - Match Confidence: [High/Medium/Low - Explain the semantic similarity]", + " - Context Notes: [How the semantic match relates to actual usage]", + " - Limitations: [Potential gaps between semantic match and practical application]", + "```", + "", + "Search Evaluation:", + "- Consider that semantic matches may not be optimal solutions", + "- Evaluate practical applicability beyond semantic similarity", + "- Look for gaps between semantic matches and actual requirements", + "- Consider alternative approaches if semantic matches are weak", + "", + "Additional Context:", + "- Note the confidence level of semantic matches", + "- Highlight where semantic similarity might be misleading", + "- Suggest verification steps before accepting matches", + "", + ].join("\n"); + } } diff --git a/src/utils/PilotPromptCreator.test.ts b/src/utils/PilotPromptCreator.test.ts index 95ddc89..a72fefd 100644 --- a/src/utils/PilotPromptCreator.test.ts +++ b/src/utils/PilotPromptCreator.test.ts @@ -1,51 +1,56 @@ -import { PilotPromptCreator } from './PilotPromptCreator'; -import { - PreviousStep, -} from "@/types"; - - -describe('PilotPromptCreator', () => { - let promptCreator: PilotPromptCreator; - - beforeEach(() => { - promptCreator = new PilotPromptCreator(); - }); - - it('should create a prompt for an intent correctly', () => { - const intent = 'tap button'; - const viewHierarchy = '