mirror of
https://github.com/appium/appium.git
synced 2026-05-18 00:59:52 -05:00
feat(appium): appium now expects extensions to use peer dependencies
The peer dependency value is stored in the `extensions.yaml` manifest for each extension as `appiumVersion` (if present). If the peer dependency is _not_ present, the user will be warned, but Appium will still try to load the extension (I have a feeling that most of the time, this will be fine). When warned, the user will receive information about available extension upgrades, if any. This required some refactors in the `lib/extension/` dir. Extension validation was previously a synchronous process, but because we now (potentially) display information to the user about upgrades (which is async), validation must also be async. This means that the `ExtensionConfig` constructor (and the constructors of its subclasses) cannot run validation. Validation must happen after the config object is instantiated, which is handled in `loadExtensions()` of `lib/extension/index.js`. The constructor signatures have changed accordingly. While extensions now soft-require a `peerDependencies.appium` field, in the monorepo, the value of this dependency is `file:../appium`. This is treated as a special case, and acts as if the dependency is just the current version of `appium` (as in its `package.json`). This is handled in `Manifest#addExtension()`. To further support this, `appium` now exports `@appium/base-driver` as `driver`, `@appium/base-plugin` as `plugin`, and `@appium/support` as `support`. `tsconfig.json` files have changed in these affected packages. In addition, a new reusable TS config file has been added for use with basic plugins. # Conflicts: # .eslintrc # packages/appium/lib/extension/extension-config.js # packages/appium/lib/extension/index.js # packages/appium/lib/extension/manifest.js # packages/appium/test/unit/extension/manifest.spec.js # packages/appium/test/unit/extension/mocks.js
This commit is contained in:
@@ -11,7 +11,17 @@
|
||||
"rules": {"func-names": "off"}
|
||||
},
|
||||
{
|
||||
"files": ["./packages/*/index.js", "./packages/*/scripts/**/*.js", "./test/*.js"],
|
||||
"files": ["packages/*/index.js", "packages/*/scripts/**/*.js", "test/*.js"],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"packages/appium/support.js",
|
||||
"packages/appium/driver.js",
|
||||
"packages/appium/plugin.js"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
@@ -12,12 +13,6 @@
|
||||
"strictNullChecks": true,
|
||||
"removeComments": false,
|
||||
"target": "es2015",
|
||||
"types": [
|
||||
"node",
|
||||
"mocha",
|
||||
"chai",
|
||||
"sinon-chai",
|
||||
"chai-as-promised"
|
||||
]
|
||||
"types": ["node", "mocha", "chai", "sinon-chai", "chai-as-promised"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"appium": ["../packages/appium"]
|
||||
},
|
||||
"types": ["webdriverio/async"]
|
||||
},
|
||||
"extends": "./tsconfig.base.json",
|
||||
"references": [{"path": "../packages/appium"}]
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from '@appium/base-driver';
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* This module is here to re-export `@appium/base-driver` for Appium extensions.
|
||||
*
|
||||
* @see https://npm.im/@appium/base-driver
|
||||
* @example
|
||||
* const { BaseDriver, errors } = require('appium/driver');
|
||||
*/
|
||||
|
||||
/** @type {import('@appium/base-driver')} */
|
||||
module.exports = require('@appium/base-driver');
|
||||
@@ -84,6 +84,8 @@ class AppiumDriver extends DriverCore {
|
||||
/** @type {AppiumServer} */
|
||||
server;
|
||||
|
||||
desiredCapConstraints = desiredCapabilityConstraints;
|
||||
|
||||
/**
|
||||
* @param {DriverOpts} opts
|
||||
*/
|
||||
@@ -98,8 +100,6 @@ class AppiumDriver extends DriverCore {
|
||||
|
||||
super(opts);
|
||||
|
||||
this.desiredCapConstraints = desiredCapabilityConstraints;
|
||||
|
||||
this.args = {...opts};
|
||||
|
||||
// allow this to happen in the background, so no `await`
|
||||
@@ -406,7 +406,7 @@ class AppiumDriver extends DriverCore {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('appium/types').DriverClass} InnerDriver
|
||||
* @param {import('@appium/base-driver').DriverClass} InnerDriver
|
||||
* @returns {Promise<DriverData[]>}}
|
||||
*/
|
||||
// eslint-disable-next-line require-await
|
||||
|
||||
@@ -324,27 +324,63 @@ class ExtensionCommand {
|
||||
* load as the main driver class, or to be able to detect incompatibilities between driver and
|
||||
* appium versions.
|
||||
*
|
||||
* @param {ExtPackageJson<ExtType>} pkgJsonData - the package.json data for a driver module, as if it had been straightforwardly 'require'd
|
||||
* @param {string} installSpec
|
||||
* @param {ExtPackageJson<ExtType>} pkgJson - the package.json data for a driver module, as if it had been straightforwardly 'require'd
|
||||
* @param {string} installSpec - Extension name/spec
|
||||
* @returns {ExtensionFields<ExtType>}
|
||||
*/
|
||||
getExtensionFields(pkgJsonData, installSpec) {
|
||||
if (!pkgJsonData.appium) {
|
||||
throw new Error(
|
||||
`Installed driver did not have an 'appium' section in its ` +
|
||||
`package.json file as expected`
|
||||
);
|
||||
}
|
||||
const {appium, name, version} = pkgJsonData;
|
||||
this.validateExtensionFields(appium, installSpec);
|
||||
getExtensionFields(pkgJson, installSpec) {
|
||||
this.validatePackageJson(pkgJson, installSpec);
|
||||
const {appium, name, version, peerDependencies} = pkgJson;
|
||||
|
||||
/** @type {unknown} */
|
||||
const result = {...appium, pkgName: name, version};
|
||||
const result = {
|
||||
...appium,
|
||||
pkgName: name,
|
||||
version,
|
||||
appiumVersion: peerDependencies?.appium,
|
||||
};
|
||||
return /** @type {ExtensionFields<ExtType>} */ (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* For any package.json fields which a particular type of extension requires, validate the
|
||||
* presence and form of those fields on the package.json data, throwing an error if anything is
|
||||
* Validates the _required_ root fields of an extension's `package.json` file.
|
||||
*
|
||||
* These required fields are:
|
||||
* - `name`
|
||||
* - `version`
|
||||
* - `appium`
|
||||
* @param {import('type-fest').PackageJson} pkgJson - `package.json` of extension
|
||||
* @param {string} installSpec - Extension name/spec
|
||||
* @throws {ReferenceError} If `package.json` has a missing or invalid field
|
||||
* @returns {pkgJson is ExtPackageJson<ExtType>}
|
||||
*/
|
||||
validatePackageJson(pkgJson, installSpec) {
|
||||
const {appium, name, version} = /** @type {ExtPackageJson<ExtType>} */ (pkgJson);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} field
|
||||
* @returns {ReferenceError}
|
||||
*/
|
||||
const createMissingFieldError = (field) =>
|
||||
new ReferenceError(
|
||||
`${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``
|
||||
);
|
||||
|
||||
if (!name) {
|
||||
throw createMissingFieldError('name');
|
||||
}
|
||||
if (!version) {
|
||||
throw createMissingFieldError('version');
|
||||
}
|
||||
if (!appium) {
|
||||
throw createMissingFieldError('appium');
|
||||
}
|
||||
|
||||
this.validateExtensionFields(appium, installSpec);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* For any `package.json` fields which a particular type of extension requires, validate the
|
||||
@@ -728,7 +764,7 @@ export {ExtensionCommand};
|
||||
/**
|
||||
* Returned by {@linkcode ExtensionCommand.getExtensionFields}
|
||||
* @template {ExtensionType} ExtType
|
||||
* @typedef {ExtMetadata<ExtType> & { pkgName: string, version: string } & import('../../types/external-manifest').CommonMetadata} ExtensionFields
|
||||
* @typedef {ExtMetadata<ExtType> & { pkgName: string, version: string, appiumVersion: string } & import('appium/types').CommonMetadata} ExtensionFields
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import PluginCommand from './plugin-command';
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import {errAndQuit, log, JSON_SPACES} from './utils';
|
||||
|
||||
const commandClasses = Object.freeze(
|
||||
export const commandClasses = Object.freeze(
|
||||
/** @type {const} */ ({
|
||||
[DRIVER_TYPE]: DriverCommand,
|
||||
[PLUGIN_TYPE]: PluginCommand,
|
||||
|
||||
@@ -31,14 +31,10 @@ export class DriverConfig extends ExtensionConfig {
|
||||
* @param {import('./manifest').Manifest} manifest - Manifest instance
|
||||
* @param {DriverConfigOptions} [opts]
|
||||
*/
|
||||
constructor(manifest, {logFn, extData} = {}) {
|
||||
constructor(manifest, {logFn} = {}) {
|
||||
super(DRIVER_TYPE, manifest, logFn);
|
||||
|
||||
this.knownAutomationNames = new Set();
|
||||
|
||||
if (extData) {
|
||||
this.validate(extData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,8 +45,8 @@ export class DriverConfig extends ExtensionConfig {
|
||||
* @throws If `manifest` already associated with a `DriverConfig`
|
||||
* @returns {DriverConfig}
|
||||
*/
|
||||
static create(manifest, {extData, logFn} = {}) {
|
||||
const instance = new DriverConfig(manifest, {logFn, extData});
|
||||
static create(manifest, {logFn} = {}) {
|
||||
const instance = new DriverConfig(manifest, {logFn});
|
||||
if (DriverConfig.getInstance(manifest)) {
|
||||
throw new Error(
|
||||
`Manifest with APPIUM_HOME ${manifest.appiumHome} already has a DriverConfig; use DriverConfig.getInstance() to retrieve it.`
|
||||
@@ -71,11 +67,10 @@ export class DriverConfig extends ExtensionConfig {
|
||||
|
||||
/**
|
||||
* Checks extensions for problems
|
||||
* @param {ExtRecord<DriverType>} exts
|
||||
*/
|
||||
validate(exts) {
|
||||
async validate() {
|
||||
this.knownAutomationNames.clear();
|
||||
return super.validate(exts);
|
||||
return await super._validate(this.manifest.getExtensionData(DRIVER_TYPE));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,7 +215,6 @@ export class DriverConfig extends ExtensionConfig {
|
||||
/**
|
||||
* @typedef DriverConfigOptions
|
||||
* @property {import('./extension-config').ExtensionLogFn} [logFn] - Optional logging function
|
||||
* @property {ManifestData['drivers']} [extData] - Extension data
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -252,7 +246,7 @@ export class DriverConfig extends ExtensionConfig {
|
||||
/**
|
||||
* Return value of {@linkcode DriverConfig.findMatchingDriver}
|
||||
* @typedef MatchedDriver
|
||||
* @property {import('appium/types').DriverClass} driver
|
||||
* @property {import('@appium/base-driver').DriverClass} driver
|
||||
* @property {string} version
|
||||
* @property {string} driverName
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import {satisfies} from 'semver';
|
||||
import {commandClasses} from '../cli/extension';
|
||||
import {APPIUM_VER} from '../config';
|
||||
import log from '../logger';
|
||||
import {
|
||||
ALLOWED_SCHEMA_EXTENSIONS,
|
||||
@@ -43,6 +46,11 @@ export class ExtensionConfig {
|
||||
/** @type {Manifest} */
|
||||
manifest;
|
||||
|
||||
/**
|
||||
* @type {import('../cli/extension-command').ExtensionListData}
|
||||
*/
|
||||
_listDataCache;
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {ExtType} extensionType - Type of extension
|
||||
@@ -70,11 +78,13 @@ export class ExtensionConfig {
|
||||
* Checks extensions for problems
|
||||
* @param {ExtRecord<ExtType>} exts - Extension data
|
||||
*/
|
||||
validate(exts) {
|
||||
async _validate(exts) {
|
||||
const foundProblems = /** @type {Record<ExtName<ExtType>,Problem[]>} */ ({});
|
||||
for (const [extName, extData] of /** @type {[ExtName<ExtType>, ExtManifest<ExtType>][]} */ (
|
||||
_.toPairs(exts)
|
||||
)) {
|
||||
await this.displayConfigWarnings(extData, extName);
|
||||
|
||||
foundProblems[extName] = [
|
||||
...this.getGenericConfigProblems(extData, extName),
|
||||
...this.getConfigProblems(extData),
|
||||
@@ -82,6 +92,7 @@ export class ExtensionConfig {
|
||||
];
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const problemSummaries = [];
|
||||
for (const [extName, problems] of _.toPairs(foundProblems)) {
|
||||
if (_.isEmpty(problems)) {
|
||||
@@ -101,8 +112,7 @@ export class ExtensionConfig {
|
||||
|
||||
if (!_.isEmpty(problemSummaries)) {
|
||||
this.log(
|
||||
`Appium encountered one or more errors while validating ` +
|
||||
`the ${this.configKey} extension file (${this.manifestPath}):`
|
||||
`Appium encountered one or more unrecoverable errors while validating ${this.configKey} found in manifest ${this.manifestPath}`
|
||||
);
|
||||
for (const summary of problemSummaries) {
|
||||
this.log(summary);
|
||||
@@ -113,11 +123,96 @@ export class ExtensionConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves listing data for extensions via command class.
|
||||
* Caches the result in {@linkcode ExtensionConfig._listDataCache}
|
||||
* @protected
|
||||
* @returns {import('../cli/extension-command').ExtensionListData}
|
||||
*/
|
||||
async getListData() {
|
||||
if (this._listDataCache) {
|
||||
return this._listDataCache;
|
||||
}
|
||||
const CommandClass = /** @type {import('../cli/extension').ExtCommand<ExtType>} */ (
|
||||
commandClasses[this.extensionType]
|
||||
);
|
||||
const cmd = new CommandClass({config: this, json: true});
|
||||
const listData = await cmd.list({showInstalled: true, showUpdates: true});
|
||||
this._listDataCache = listData;
|
||||
return listData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays non-"fatal" warnings for the extension
|
||||
*
|
||||
* This method uses Appium's logger, since the default extension logger uses the `error` log level.
|
||||
*
|
||||
* @param {ExtManifest<ExtType>} extData
|
||||
* @param {ExtName<ExtType>} extName
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async displayConfigWarnings(extData, extName) {
|
||||
const {appiumVersion, installSpec, installType, pkgName} = extData;
|
||||
|
||||
if (!_.isString(installSpec)) {
|
||||
log.warn(
|
||||
`${_.capitalize(
|
||||
this.extensionType
|
||||
)} "${extName}" (package \`${pkgName}\`) has an invalid or missing \`installSpec\` property in \`extensions.yaml\`; this may cause upgrades done via the \`appium\` CLI to fail.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!INSTALL_TYPES.has(installType)) {
|
||||
log.warn(
|
||||
`${_.capitalize(
|
||||
this.extensionType
|
||||
)} "${extName}" (package \`${pkgName}\`) has an invalid or missing \`installType\` property in \`extensions.yaml\`; this may cause upgrades done via the \`appium\` CLI to fail.`
|
||||
);
|
||||
extData.installType = INSTALL_TYPE_NPM;
|
||||
}
|
||||
if (_.isString(appiumVersion) && !satisfies(APPIUM_VER, appiumVersion)) {
|
||||
const listData = await this.getListData();
|
||||
|
||||
if (listData[extName]) {
|
||||
const extListData =
|
||||
/** @type {import('../cli/extension-command').InstalledExtensionListData} */ (
|
||||
listData[extName]
|
||||
);
|
||||
const {unsafeUpdateVersion, updateVersion, upToDate} = extListData;
|
||||
if (!upToDate) {
|
||||
const upgradeText =
|
||||
unsafeUpdateVersion === updateVersion
|
||||
? `v${updateVersion}`
|
||||
: `v${updateVersion} or (potentially unsafe) v${unsafeUpdateVersion}`;
|
||||
log.warn(
|
||||
`${_.capitalize(
|
||||
this.extensionType
|
||||
)} "${extName}" (package \`${pkgName}\`) may be incompatible with the current version of Appium (v${APPIUM_VER}) due to its peer dependency on older Appium v${appiumVersion}. Please upgrade \`${pkgName}\` to ${upgradeText}.`
|
||||
);
|
||||
} else {
|
||||
log.warn(
|
||||
`${_.capitalize(
|
||||
this.extensionType
|
||||
)} "${extName}" (package \`${pkgName}\`) may be incompatible with the current version of Appium (v${APPIUM_VER}) due to its peer dependency on older Appium v${appiumVersion}. Please ask the developer of \`${pkgName}\` to update the peer dependency on Appium to ${APPIUM_VER}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (!_.isString(appiumVersion)) {
|
||||
log.warn(
|
||||
`${_.capitalize(
|
||||
this.extensionType
|
||||
)} "${extName}" (package \`${pkgName}\`) may be incompatible with the current version of Appium (v${APPIUM_VER}) due to an invalid or missing peer dependency on Appium. Please ask the developer of \`${pkgName}\` to add a peer dependency on \`appium@${APPIUM_VER}\`.`
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns list of unrecoverable errors (if any) for the given extension _if_ it has a `schema` property.
|
||||
*
|
||||
* @param {ExtManifest<ExtType>} extData - Extension data (from manifest)
|
||||
* @param {ExtName<ExtType>} extName - Extension name (from manifest)
|
||||
* @returns {Problem[]}
|
||||
*/
|
||||
getSchemaProblems(extData, extName) {
|
||||
/** @type {Problem[]} */
|
||||
const problems = [];
|
||||
const {schema: argSchemaPath} = extData;
|
||||
if (ExtensionConfig.extDataHasSchema(extData)) {
|
||||
@@ -159,43 +254,33 @@ export class ExtensionConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExtManifest<ExtType>} extData
|
||||
* @param {ExtName<ExtType>} extName
|
||||
* Return a list of generic unrecoverable errors for the given extension
|
||||
* @param {ExtManifest<ExtType>} extData - Extension data (from manifest)
|
||||
* @param {ExtName<ExtType>} extName - Extension name (from manifest)
|
||||
* @returns {Problem[]}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getGenericConfigProblems(extData, extName) {
|
||||
const {version, pkgName, installSpec, installType, mainClass} = extData;
|
||||
const {version, pkgName, mainClass} = extData;
|
||||
const problems = [];
|
||||
|
||||
if (!_.isString(version)) {
|
||||
problems.push({err: 'Missing or incorrect version', val: version});
|
||||
problems.push({
|
||||
err: `Invalid or missing \`version\` field in my \`package.json\` and/or \`extensions.yaml\` (must be a string)`,
|
||||
val: version,
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isString(pkgName)) {
|
||||
problems.push({
|
||||
err: 'Missing or incorrect NPM package name',
|
||||
err: `Invalid or missing \`name\` field in my \`package.json\` and/or \`extensions.yaml\` (must be a string)`,
|
||||
val: pkgName,
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isString(installSpec)) {
|
||||
problems.push({
|
||||
err: 'Missing or incorrect installation spec',
|
||||
val: installSpec,
|
||||
});
|
||||
}
|
||||
|
||||
if (!INSTALL_TYPES.has(installType)) {
|
||||
problems.push({
|
||||
err: 'Missing or incorrect install type',
|
||||
val: installType,
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isString(mainClass)) {
|
||||
problems.push({
|
||||
err: 'Missing or incorrect driver class name',
|
||||
err: `Invalid or missing \`appium.mainClass\` field in my \`package.json\` and/or \`mainClass\` field in \`extensions.yaml\` (must be a string)`,
|
||||
val: mainClass,
|
||||
});
|
||||
}
|
||||
@@ -398,7 +483,7 @@ export {INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('./manifest').Manifest} Manifest
|
||||
*/
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import log from '../logger';
|
||||
import {DriverConfig} from './driver-config';
|
||||
import {Manifest} from './manifest';
|
||||
import {PluginConfig} from './plugin-config';
|
||||
import B from 'bluebird';
|
||||
|
||||
/**
|
||||
* Loads extensions and creates `ExtensionConfig` instances.
|
||||
@@ -18,11 +19,11 @@ import {PluginConfig} from './plugin-config';
|
||||
*/
|
||||
export async function loadExtensions(appiumHome) {
|
||||
const manifest = Manifest.getInstance(appiumHome);
|
||||
const {drivers, plugins} = await manifest.read();
|
||||
const driverConfig =
|
||||
DriverConfig.getInstance(manifest) ?? DriverConfig.create(manifest, {extData: drivers});
|
||||
const pluginConfig =
|
||||
PluginConfig.getInstance(manifest) ?? PluginConfig.create(manifest, {extData: plugins});
|
||||
await manifest.read();
|
||||
const driverConfig = DriverConfig.getInstance(manifest) ?? DriverConfig.create(manifest);
|
||||
const pluginConfig = PluginConfig.getInstance(manifest) ?? PluginConfig.create(manifest);
|
||||
|
||||
await B.all([driverConfig.validate(), pluginConfig.validate()]);
|
||||
return {driverConfig, pluginConfig};
|
||||
}
|
||||
|
||||
@@ -91,6 +92,6 @@ export function getActiveDrivers(driverConfig, useDrivers = []) {
|
||||
|
||||
/**
|
||||
* @typedef ExtensionConfigs
|
||||
* @property {DriverConfig} driverConfig
|
||||
* @property {PluginConfig} pluginConfig
|
||||
* @property {import('./driver-config').DriverConfig} driverConfig
|
||||
* @property {import('./plugin-config').PluginConfig} pluginConfig
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import log from '../logger';
|
||||
import {INSTALL_TYPE_NPM} from './extension-config';
|
||||
import {packageDidChange} from './package-changed';
|
||||
import {APPIUM_VER} from '../config';
|
||||
|
||||
/**
|
||||
* Default depth to search in directory tree for whatever it is we're looking for.
|
||||
@@ -226,12 +227,15 @@ export class Manifest {
|
||||
* @returns {boolean} - `true` upon success, `false` if the extension is already registered.
|
||||
*/
|
||||
addExtensionFromPackage(pkgJson, pkgPath) {
|
||||
const extensionPath = path.dirname(pkgPath);
|
||||
|
||||
/**
|
||||
* @type {InternalMetadata}
|
||||
*/
|
||||
const internal = {
|
||||
pkgName: pkgJson.name,
|
||||
version: pkgJson.version,
|
||||
appiumVersion: pkgJson.peerDependencies?.appium,
|
||||
installType: INSTALL_TYPE_NPM,
|
||||
installSpec: `${pkgJson.name}@${pkgJson.version}`,
|
||||
};
|
||||
@@ -256,7 +260,7 @@ export class Manifest {
|
||||
return false;
|
||||
} else {
|
||||
throw new TypeError(
|
||||
`The extension in ${path.dirname(pkgPath)} is neither a valid driver nor a valid plugin.`
|
||||
`The extension in ${extensionPath} is neither a valid driver nor a valid plugin.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -270,10 +274,15 @@ export class Manifest {
|
||||
* @param {ExtType} extType - `driver` or `plugin`
|
||||
* @param {string} extName - Name of extension
|
||||
* @param {ExtManifest<ExtType>} extData - Extension metadata
|
||||
* @returns {void}
|
||||
* @returns {ExtManifest<ExtType>} A clone of `extData`, potentially with a mutated `appiumVersion` field
|
||||
*/
|
||||
addExtension(extType, extName, extData) {
|
||||
this._data[`${extType}s`][extName] = extData;
|
||||
const data = _.clone(extData);
|
||||
if (data.appiumVersion?.startsWith('file:..')) {
|
||||
data.appiumVersion = APPIUM_VER;
|
||||
}
|
||||
this._data[`${extType}s`][extName] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,6 +335,7 @@ export class Manifest {
|
||||
log.debug(`Reading ${this._manifestPath}...`);
|
||||
const yaml = await fs.readFile(this._manifestPath, 'utf8');
|
||||
data = YAML.parse(yaml);
|
||||
log.debug(`Parsed manifest file: ${JSON.stringify(data, null, 2)}`);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
data = _.cloneDeep(INITIAL_MANIFEST_DATA);
|
||||
|
||||
@@ -27,12 +27,12 @@ export class PluginConfig extends ExtensionConfig {
|
||||
* @param {Manifest} manifest - IO object
|
||||
* @param {PluginConfigOptions} [opts]
|
||||
*/
|
||||
constructor(manifest, {extData, logFn} = {}) {
|
||||
constructor(manifest, {logFn} = {}) {
|
||||
super(PLUGIN_TYPE, manifest, logFn);
|
||||
}
|
||||
|
||||
if (extData) {
|
||||
this.validate(extData);
|
||||
}
|
||||
async validate() {
|
||||
return await super._validate(this.manifest.getExtensionData(PLUGIN_TYPE));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,8 +43,8 @@ export class PluginConfig extends ExtensionConfig {
|
||||
* @throws If `manifest` already associated with a `PluginConfig`
|
||||
* @returns {PluginConfig}
|
||||
*/
|
||||
static create(manifest, {extData, logFn} = {}) {
|
||||
const instance = new PluginConfig(manifest, {logFn, extData});
|
||||
static create(manifest, {logFn} = {}) {
|
||||
const instance = new PluginConfig(manifest, {logFn});
|
||||
if (PluginConfig.getInstance(manifest)) {
|
||||
throw new Error(
|
||||
`Manifest with APPIUM_HOME ${manifest.appiumHome} already has a PluginConfig; use PluginConfig.getInstance() to retrieve it.`
|
||||
@@ -105,7 +105,6 @@ export class PluginConfig extends ExtensionConfig {
|
||||
/**
|
||||
* @typedef PluginConfigOptions
|
||||
* @property {import('./extension-config').ExtensionLogFn} [logFn] - Optional logging function
|
||||
* @property {import('appium/types').PluginRecord} [extData] - Extension data
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -384,9 +384,9 @@ export {finalizeSchema, getSchema, validate} from './schema/schema';
|
||||
export {main, init, resolveAppiumHome};
|
||||
|
||||
/**
|
||||
* @typedef {import('appium/types').DriverType} DriverType
|
||||
* @typedef {import('appium/types').PluginType} PluginType
|
||||
* @typedef {import('appium/types').DriverClass} DriverClass
|
||||
* @typedef {import('@appium/types').DriverType} DriverType
|
||||
* @typedef {import('@appium/types').PluginType} PluginType
|
||||
* @typedef {import('@appium/base-driver').DriverClass} DriverClass
|
||||
* @typedef {import('appium/types').PluginClass} PluginClass
|
||||
* @typedef {import('appium/types').WithServerSubcommand} WithServerSubcommand
|
||||
*/
|
||||
@@ -398,7 +398,7 @@ export {main, init, resolveAppiumHome};
|
||||
|
||||
/**
|
||||
* @typedef ServerInitData
|
||||
* @property {AppiumDriver} appiumDriver - The Appium driver
|
||||
* @property {import('./appium').AppiumDriver} appiumDriver - The Appium driver
|
||||
* @property {import('appium/types').ParsedArgs} parsedArgs - The parsed arguments
|
||||
*/
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
"lib",
|
||||
"build",
|
||||
"index.js",
|
||||
"driver.*",
|
||||
"support.*",
|
||||
"plugin.*",
|
||||
"test.*",
|
||||
"scripts/postinstall.js",
|
||||
"types"
|
||||
],
|
||||
@@ -58,6 +62,7 @@
|
||||
"@appium/docutils": "file:../docutils",
|
||||
"@appium/schema": "file:../schema",
|
||||
"@appium/support": "file:../support",
|
||||
"@appium/test-support": "file:../test-support",
|
||||
"@babel/runtime": "7.17.9",
|
||||
"@sidvind/better-ajv-errors": "2.0.0",
|
||||
"ajv": "8.11.0",
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from '@appium/base-plugin';
|
||||
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* This module is here to re-export `@appium/base-plugin` for Appium extensions.
|
||||
*
|
||||
* @see https://npm.im/@appium/base-plugin
|
||||
* @example
|
||||
* const { BasePlugin } = require('appium/plugin');
|
||||
*/
|
||||
|
||||
module.exports = require('@appium/base-plugin');
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from '@appium/support';
|
||||
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* This module is here to re-export `@appium/support` for Appium extensions.
|
||||
*
|
||||
* @see https://npm.im/@appium/support
|
||||
* @example
|
||||
* const { fs, npm } = require('appium/support');
|
||||
*/
|
||||
|
||||
module.exports = require('@appium/support');
|
||||
@@ -40,7 +40,7 @@ describe('CLI behavior', function () {
|
||||
this.timeout(30000);
|
||||
});
|
||||
|
||||
describe('when appium is a dependency', function () {
|
||||
describe('when appium is a dependency of the project in the current working directory', function () {
|
||||
/** @type {string} */
|
||||
let hashPath;
|
||||
/** @type {string} */
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('FakeDriver - via HTTP', function () {
|
||||
let appiumHome;
|
||||
// since we update the FakeDriver.prototype below, make sure we update the FakeDriver which is
|
||||
// actually going to be required by Appium
|
||||
/** @type {import('appium/types').DriverClass} */
|
||||
/** @type {import('@appium/types').DriverClass} */
|
||||
let FakeDriver;
|
||||
/** @type {string} */
|
||||
let testServerBaseSessionUrl;
|
||||
|
||||
@@ -1,11 +1,211 @@
|
||||
// @ts-check
|
||||
import {DRIVER_TYPE} from '../../../lib/constants';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
import {rewiremock} from '../../helpers';
|
||||
import {initMocks} from './mocks';
|
||||
|
||||
const {expect} = chai;
|
||||
|
||||
describe('ExtensionConfig', function () {
|
||||
describe('getGenericConfigProblems()', function () {
|
||||
it('should have some tests');
|
||||
let sandbox;
|
||||
|
||||
/** @type {typeof import('appium/lib/extension/extension-config').ExtensionConfig} */
|
||||
let ExtensionConfig;
|
||||
|
||||
/** @type {typeof import('appium/lib/extension/manifest').Manifest} */
|
||||
let Manifest;
|
||||
|
||||
/** @type {import('./mocks').MockAppiumSupport} */
|
||||
let MockAppiumSupport;
|
||||
|
||||
beforeEach(function () {
|
||||
let overrides;
|
||||
({MockAppiumSupport, overrides, sandbox} = initMocks());
|
||||
({ExtensionConfig} = rewiremock.proxy(
|
||||
() => require('../../../lib/extension/extension-config'),
|
||||
overrides
|
||||
));
|
||||
({Manifest} = rewiremock.proxy(() => require('../../../lib/extension/manifest'), overrides));
|
||||
|
||||
MockAppiumSupport.fs.readPackageJsonFrom.returns({version: '2.0.0'});
|
||||
});
|
||||
|
||||
describe('validate()', function () {
|
||||
it('should have some tests');
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('constructor', function () {});
|
||||
|
||||
describe('instance method', function () {
|
||||
/** @type {import('appium/lib/extension/extension-config').ExtensionConfig<DriverType>} */
|
||||
let config;
|
||||
|
||||
let extData;
|
||||
|
||||
beforeEach(function () {
|
||||
config = new ExtensionConfig(DRIVER_TYPE, new Manifest('/some/path'));
|
||||
extData = {
|
||||
version: '1.0.0',
|
||||
automationName: 'Derp',
|
||||
mainClass: 'SomeClass',
|
||||
pkgName: 'derp',
|
||||
platformNames: ['dogs', 'cats'],
|
||||
installSpec: 'derp',
|
||||
installType: 'npm',
|
||||
appiumVersion: APPIUM_VER,
|
||||
};
|
||||
config.addExtension('derp', extData);
|
||||
});
|
||||
|
||||
describe('getGenericConfigProblems()', function () {
|
||||
describe('when there are no problems with the extension data', function () {
|
||||
it('should return an empty array', function () {
|
||||
expect(config.getGenericConfigProblems(extData, 'derp')).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension data is missing a "pkgName" field', function () {
|
||||
beforeEach(function () {
|
||||
delete extData.pkgName;
|
||||
});
|
||||
|
||||
it('should return a problem', function () {
|
||||
expect(config.getGenericConfigProblems(extData, 'derp')).to.eql([
|
||||
{
|
||||
err: 'Invalid or missing `name` field in my `package.json` and/or `extensions.yaml` (must be a string)',
|
||||
val: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension data is missing a "version" field', function () {
|
||||
beforeEach(function () {
|
||||
delete extData.version;
|
||||
});
|
||||
|
||||
it('should return a problem', function () {
|
||||
expect(config.getGenericConfigProblems(extData, 'derp')).to.eql([
|
||||
{
|
||||
err: 'Invalid or missing `version` field in my `package.json` and/or `extensions.yaml` (must be a string)',
|
||||
val: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension data is missing a "appium.mainClass" field', function () {
|
||||
beforeEach(function () {
|
||||
delete extData.mainClass;
|
||||
});
|
||||
|
||||
it('should return a problem', function () {
|
||||
expect(config.getGenericConfigProblems(extData, 'derp')).to.eql([
|
||||
{
|
||||
err: 'Invalid or missing `appium.mainClass` field in my `package.json` and/or `mainClass` field in `extensions.yaml` (must be a string)',
|
||||
val: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayConfigWarnings()', function () {
|
||||
/** @type {ExtManifest<DriverType>} */
|
||||
const extData = {
|
||||
version: '1.0.0',
|
||||
automationName: 'Derp',
|
||||
mainClass: 'SomeClass',
|
||||
pkgName: 'derp',
|
||||
platformNames: ['dogs', 'cats'],
|
||||
installSpec: 'derp',
|
||||
installType: 'npm',
|
||||
appiumVersion: APPIUM_VER,
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {ExtensionConfig<DriverType>}
|
||||
*/
|
||||
let config;
|
||||
|
||||
beforeEach(function () {
|
||||
const manifest = Manifest.getInstance('/some/path');
|
||||
manifest.addExtension(DRIVER_TYPE, 'derp', extData);
|
||||
config = new ExtensionConfig(DRIVER_TYPE, manifest);
|
||||
});
|
||||
|
||||
describe('when the extension data is missing an `installSpec` field', function () {
|
||||
beforeEach(function () {
|
||||
delete extData.installSpec;
|
||||
});
|
||||
|
||||
it('should log a warning', async function () {
|
||||
await config.displayConfigWarnings(extData, 'derp');
|
||||
expect(MockAppiumSupport.logger.getLogger().warn).to.have.been.calledWith(
|
||||
'Driver "derp" (package `derp`) has an invalid or missing `installSpec` property in `extensions.yaml`; this may cause upgrades done via the `appium` CLI to fail.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension data is missing an `installType` field', function () {
|
||||
beforeEach(function () {
|
||||
delete extData.installType;
|
||||
});
|
||||
|
||||
it('should log a warning', async function () {
|
||||
await config.displayConfigWarnings(extData, 'derp');
|
||||
expect(MockAppiumSupport.logger.getLogger().warn).to.have.been.calledWith(
|
||||
'Driver "derp" (package `derp`) has an invalid or missing `installType` property in `extensions.yaml`; this may cause upgrades done via the `appium` CLI to fail.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension data is missing an `appiumVersion` field', function () {
|
||||
beforeEach(function () {
|
||||
delete extData.appiumVersion;
|
||||
});
|
||||
|
||||
it('should log a warning', async function () {
|
||||
await config.displayConfigWarnings(extData, 'derp');
|
||||
expect(MockAppiumSupport.logger.getLogger().warn).to.have.been.calledWith(
|
||||
`Driver "derp" (package \`derp\`) may be incompatible with the current version of Appium (v${APPIUM_VER}) due to an invalid or missing peer dependency on Appium. Please ask the developer of \`derp\` to add a peer dependency on \`appium@${APPIUM_VER}\`.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension data has an `appiumVersion` field which does not satisfy the current version of Appium, and an upgrade is available', function () {
|
||||
beforeEach(function () {
|
||||
extData.appiumVersion = '1.9.9';
|
||||
});
|
||||
it('should log a warning', async function () {
|
||||
await config.displayConfigWarnings(extData, 'derp');
|
||||
expect(MockAppiumSupport.logger.getLogger().warn).to.have.been.calledWith(
|
||||
`Driver "derp" (package \`derp\`) may be incompatible with the current version of Appium (v${APPIUM_VER}) due to its peer dependency on older Appium v${extData.appiumVersion}. Please upgrade \`derp\` to v1.1.0 or (potentially unsafe) v2.0.0.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension data has an `appiumVersion` field which does not satisfy the current version of Appium, and no upgrade is available', function () {
|
||||
beforeEach(function () {
|
||||
extData.appiumVersion = '1.9.9';
|
||||
MockAppiumSupport.util.compareVersions.returns(false);
|
||||
MockAppiumSupport.npm.getLatestSafeUpgradeVersion.resolves('1.0.0');
|
||||
MockAppiumSupport.npm.getLatestVersion.resolves('1.0.0');
|
||||
});
|
||||
it('should log a warning', async function () {
|
||||
await config.displayConfigWarnings(extData, 'derp');
|
||||
expect(MockAppiumSupport.logger.getLogger().warn).to.have.been.calledWith(
|
||||
`Driver "derp" (package \`derp\`) may be incompatible with the current version of Appium (v${APPIUM_VER}) due to its peer dependency on older Appium v${extData.appiumVersion}. Please ask the developer of \`derp\` to update the peer dependency on Appium to ${APPIUM_VER}.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate()', function () {
|
||||
it('should have some tests');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import('appium/types').DriverType} DriverType
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import {promises as fs} from 'fs';
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../../../lib/constants';
|
||||
import {resolveFixture, rewiremock} from '../../helpers';
|
||||
import {initMocks} from './mocks';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
|
||||
const {expect} = chai;
|
||||
|
||||
@@ -35,7 +36,6 @@ describe('Manifest', function () {
|
||||
let overrides;
|
||||
({MockPackageChanged, MockAppiumSupport, overrides, sandbox} = initMocks());
|
||||
MockAppiumSupport.fs.readFile.resolves(yamlFixture);
|
||||
|
||||
({Manifest} = rewiremock.proxy(() => require('../../../lib/extension/manifest'), overrides));
|
||||
|
||||
Manifest.getInstance.cache = new Map();
|
||||
@@ -240,6 +240,7 @@ describe('Manifest', function () {
|
||||
platformNames: ['dogs', 'cats'],
|
||||
installSpec: 'derp',
|
||||
installType: 'npm',
|
||||
appiumVersion: '2.0.0',
|
||||
};
|
||||
|
||||
beforeEach(async function () {
|
||||
@@ -298,11 +299,14 @@ describe('Manifest', function () {
|
||||
platformNames: ['dogs', 'cats'],
|
||||
installSpec: 'derp',
|
||||
installType: 'npm',
|
||||
appiumVersion: '2.0.0',
|
||||
};
|
||||
|
||||
it('should add the extension to the internal data object', function () {
|
||||
manifest.addExtension('driver', 'foo', extData);
|
||||
expect(manifest.getExtensionData('driver').foo).to.equal(extData);
|
||||
it('should add a clone of the extension manifest to the internal data object', function () {
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', extData);
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE).foo)
|
||||
.to.eql(extData)
|
||||
.and.not.to.equal(extData);
|
||||
});
|
||||
|
||||
describe('when existing extension added', function () {
|
||||
@@ -317,12 +321,55 @@ describe('Manifest', function () {
|
||||
...extData,
|
||||
automationName: 'BLAAHAH',
|
||||
};
|
||||
manifest.addExtension('driver', 'foo', expected);
|
||||
expect(manifest.getExtensionData('driver').foo).to.equal(expected);
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', expected);
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE).foo).to.eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension has no peer dependency on `appium`', function () {
|
||||
beforeEach(function () {
|
||||
delete extData.appiumVersion;
|
||||
});
|
||||
|
||||
it('should work anyway', function () {
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', extData);
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE).foo).to.not.have.property('appiumVersion');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension has a peer dependency on `appium`, but it references a filepath', function () {
|
||||
beforeEach(function () {
|
||||
extData.appiumVersion = 'file:../appium';
|
||||
});
|
||||
|
||||
it('should set `appiumVersion` to the current appium version', function () {
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', extData);
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE).foo.appiumVersion).to.equal(APPIUM_VER);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExtensionData()', function () {
|
||||
/** @type {ExtManifest<DriverType>} */
|
||||
const extData = {
|
||||
version: '1.0.0',
|
||||
automationName: 'Derp',
|
||||
mainClass: 'SomeClass',
|
||||
pkgName: 'derp',
|
||||
platformNames: ['dogs', 'cats'],
|
||||
installSpec: 'derp',
|
||||
installType: 'npm',
|
||||
appiumVersion: '2.0.0',
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', extData);
|
||||
});
|
||||
|
||||
it('should return all extension data for an extension type', function () {
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE)).to.eql({foo: extData});
|
||||
});
|
||||
});
|
||||
describe('addExtensionFromPackage()', function () {
|
||||
describe('when provided a valid package.json for a driver and its path', function () {
|
||||
/** @type {ExtPackageJson<DriverType>} */
|
||||
@@ -338,12 +385,15 @@ describe('Manifest', function () {
|
||||
platformNames: ['dogs', 'cats'],
|
||||
driverName: 'myDriver',
|
||||
},
|
||||
peerDependencies: {
|
||||
appium: '2.0.0',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should add an extension to the internal data', function () {
|
||||
manifest.addExtensionFromPackage(packageJson, '/some/path/to/package.json');
|
||||
expect(manifest.getExtensionData('driver')).to.deep.equal({
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE)).to.deep.equal({
|
||||
myDriver: {
|
||||
automationName: 'derp',
|
||||
mainClass: 'SomeClass',
|
||||
@@ -352,6 +402,7 @@ describe('Manifest', function () {
|
||||
version: '1.0.0',
|
||||
installType: 'npm',
|
||||
installSpec: 'derp@1.0.0',
|
||||
appiumVersion: '2.0.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -384,6 +435,9 @@ describe('Manifest', function () {
|
||||
mainClass: 'SomeClass',
|
||||
pluginName: 'myPlugin',
|
||||
},
|
||||
peerDependencies: {
|
||||
appium: '2.0.0',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -396,6 +450,7 @@ describe('Manifest', function () {
|
||||
version: '1.0.0',
|
||||
installType: 'npm',
|
||||
installSpec: 'derp@1.0.0',
|
||||
appiumVersion: '2.0.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -428,6 +483,34 @@ describe('Manifest', function () {
|
||||
).to.throw(/neither a valid driver nor a valid plugin/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the extension has an appium peer dependency beginning with `file:..`', function () {
|
||||
/** @type {ExtPackageJson<DriverType>} */
|
||||
let packageJson;
|
||||
|
||||
beforeEach(function () {
|
||||
packageJson = {
|
||||
name: 'derp',
|
||||
version: '1.0.0',
|
||||
appium: {
|
||||
automationName: 'derp',
|
||||
mainClass: 'SomeClass',
|
||||
platformNames: ['dogs', 'cats'],
|
||||
driverName: 'myDriver',
|
||||
},
|
||||
peerDependencies: {
|
||||
appium: 'file:../appium',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should set the appiumVersion to the current Appium version', function () {
|
||||
manifest.addExtensionFromPackage(packageJson, '/some/path/to/package.json');
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE).myDriver.appiumVersion).to.equal(
|
||||
APPIUM_VER
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncWithInstalledExtensions()', function () {
|
||||
@@ -465,6 +548,9 @@ describe('Manifest', function () {
|
||||
platformNames: ['dogs', 'cats'],
|
||||
driverName: 'myDriver',
|
||||
},
|
||||
peerDependencies: {
|
||||
appium: '2.0.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import path from 'path';
|
||||
import {createSandbox} from 'sinon';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
|
||||
export function initMocks(sandbox = createSandbox()) {
|
||||
/**
|
||||
@@ -25,6 +26,12 @@ export function initMocks(sandbox = createSandbox()) {
|
||||
})
|
||||
),
|
||||
mkdirp: /** @type {MockAppiumSupportFs['mkdirp']} */ (sandbox.stub().resolves()),
|
||||
readPackageJsonFrom: /** @type {MockAppiumSupportFs['readPackageJsonFrom']} */ (
|
||||
sandbox.stub().returns({version: APPIUM_VER, engines: {node: '>=12'}})
|
||||
),
|
||||
findRoot: /** @type {MockAppiumSupportFs['findRoot']} */ (
|
||||
sandbox.stub().returns(path.join(__dirname, '..', '..', '..'))
|
||||
),
|
||||
},
|
||||
env: {
|
||||
resolveAppiumHome: /** @type {MockAppiumSupportEnv['resolveAppiumHome']} */ (
|
||||
@@ -48,9 +55,27 @@ export function initMocks(sandbox = createSandbox()) {
|
||||
},
|
||||
logger: {
|
||||
getLogger: /** @type {MockAppiumSupportLogger['getLogger']} */ (
|
||||
sandbox
|
||||
.stub()
|
||||
.returns(sandbox.stub(new global.console.Console(process.stdout, process.stderr)))
|
||||
sandbox.stub().callsFake(() => MockAppiumSupport.logger.__logger)
|
||||
),
|
||||
__logger: sandbox.stub(new global.console.Console(process.stdout, process.stderr)),
|
||||
},
|
||||
system: {
|
||||
isWindows: /** @type {MockAppiumSupportSystem['isWindows']} */ (
|
||||
sandbox.stub().returns(false)
|
||||
),
|
||||
},
|
||||
npm: {
|
||||
getLatestVersion: /** @type {MockAppiumSupportNpm['getLatestVersion']} */ (
|
||||
sandbox.stub().resolves('2.0.0')
|
||||
),
|
||||
getLatestSafeUpgradeVersion:
|
||||
/** @type {MockAppiumSupportNpm['getLatestSafeUpgradeVersion']} */ (
|
||||
sandbox.stub().resolves('1.1.0')
|
||||
),
|
||||
},
|
||||
util: {
|
||||
compareVersions: /** @type {MockAppiumSupportUtil['compareVersions']} */ (
|
||||
sandbox.stub().returns(true)
|
||||
),
|
||||
},
|
||||
};
|
||||
@@ -98,12 +123,16 @@ export function initMocks(sandbox = createSandbox()) {
|
||||
* @property {MockAppiumSupportLogger} logger
|
||||
* @property {MockAppiumSupportFs} fs
|
||||
* @property {MockAppiumSupportEnv} env
|
||||
* @property {MockAppiumSupportSystem} system
|
||||
* @property {MockAppiumSupportNpm} npm
|
||||
* @property {MockAppiumSupportUtil} util
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mock of package `@appium/support`'s `logger` module
|
||||
* @typedef MockAppiumSupportLogger
|
||||
* @property {sinon.SinonStub<[string?], typeof console>} getLogger
|
||||
* @property {sinon.SinonStub<[string?], Console>} getLogger
|
||||
* @property {sinon.SinonStubbedInstance<Console>} __logger
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -113,6 +142,8 @@ export function initMocks(sandbox = createSandbox()) {
|
||||
* @property {sinon.SinonStubbedMember<import('@appium/support/lib/fs')['fs']['writeFile']>} writeFile
|
||||
* @property {sinon.SinonStubbedMember<import('@appium/support/lib/fs')['fs']['walk']>} walk
|
||||
* @property {sinon.SinonStubbedMember<import('@appium/support/lib/fs')['fs']['mkdirp']>} mkdirp
|
||||
* @property {sinon.SinonStubbedMember<import('@appium/support/lib/fs')['fs']['readPackageJsonFrom']>} readPackageJsonFrom
|
||||
* @property {sinon.SinonStubbedMember<import('@appium/support/lib/fs')['fs']['findRoot']>} findRoot
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -125,6 +156,22 @@ export function initMocks(sandbox = createSandbox()) {
|
||||
* @property {import('@appium/support/lib/env').NormalizedPackageJson} __pkg
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef MockAppiumSupportSystem
|
||||
* @property {sinon.SinonStubbedMember<import('@appium/support/lib/system').isWindows>} isWindows
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef MockAppiumSupportNpm
|
||||
* @property {sinon.SinonStubbedMember<import('@appium/support/lib/npm').NPM['getLatestVersion']>} getLatestVersion
|
||||
* @property {sinon.SinonStubbedMember<import('@appium/support/lib/npm').NPM['getLatestSafeUpgradeVersion']>} getLatestSafeUpgradeVersion
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef MockAppiumSupportUtil
|
||||
* @property {sinon.SinonStubbedMember<import('@appium/support/lib/util').compareVersions>} compareVersions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mock of package `package-changed`
|
||||
* @typedef MockPackageChanged
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"paths": {
|
||||
"@appium/support": ["../support"],
|
||||
"@appium/base-driver": ["../base-driver"],
|
||||
"@appium/base-plugin": ["../base-plugin"],
|
||||
"@appium/types": ["../types"],
|
||||
"@appium/schema": ["../schema"],
|
||||
"appium": ["."]
|
||||
@@ -15,8 +16,9 @@
|
||||
"include": ["lib", "types"],
|
||||
"references": [
|
||||
{"path": "../support"},
|
||||
{"path": "../base-driver"},
|
||||
{"path": "../types"},
|
||||
{"path": "../schema"}
|
||||
{"path": "../schema"},
|
||||
{"path": "../base-driver"},
|
||||
{"path": "../base-plugin"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ export interface InternalMetadata {
|
||||
* Whatever the user typed as the extension to install. May be derived from `package.json`
|
||||
*/
|
||||
installSpec: string;
|
||||
/**
|
||||
* Maximum version of Appium that this extension is compatible with.
|
||||
*
|
||||
* If `undefined`, we'll try anyway.
|
||||
*/
|
||||
appiumVersion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import type {BaseDriverBase} from '@appium/base-driver/lib/basedriver/driver';
|
||||
import {Class, Driver, ExternalDriver} from '@appium/types';
|
||||
import {DriverType, ExtensionType, PluginType} from '.';
|
||||
|
||||
export type DriverClass = BaseDriverBase<ExternalDriver, ExternalDriverStatic>;
|
||||
|
||||
/**
|
||||
* Additional static props for external driver classes
|
||||
*/
|
||||
export interface ExternalDriverStatic {
|
||||
driverName: string;
|
||||
}
|
||||
import {DriverClass} from '@appium/base-driver';
|
||||
import {Class, Driver, DriverType, PluginType} from '@appium/types';
|
||||
|
||||
/**
|
||||
* TODO: This should be derived from the base plugin.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import type {SchemaObject} from 'ajv';
|
||||
import type {PackageJson, SetRequired} from 'type-fest';
|
||||
import {DriverType, ExtensionType, PluginType} from './index';
|
||||
import {DriverType, ExtensionType, PluginType} from '@appium/types';
|
||||
|
||||
/**
|
||||
* This is what is allowed in the `appium.schema` prop of an extension's `package.json`.
|
||||
@@ -49,9 +49,16 @@ export type ExtMetadata<ExtType extends ExtensionType> = (ExtType extends Driver
|
||||
|
||||
/**
|
||||
* A `package.json` containing extension metadata.
|
||||
* Required fields are `name`, `version`, and `appium`.
|
||||
* Must have the following properties:
|
||||
* - `name`: the name of the extension
|
||||
* - `version`: the version of the extension
|
||||
* - `appium`: the metadata for the extension
|
||||
* - `peerDependencies.appium`: the maximum compatible version of Appium
|
||||
*/
|
||||
export type ExtPackageJson<ExtType extends ExtensionType> = SetRequired<
|
||||
PackageJson,
|
||||
'name' | 'version'
|
||||
> & {appium: ExtMetadata<ExtType>};
|
||||
> & {
|
||||
appium: ExtMetadata<ExtType>;
|
||||
peerDependencies: {appium: string; [key: string]: string};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"exclude": [
|
||||
"packages/**/test/**",
|
||||
"packages/**/build/**"
|
||||
],
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user