diff --git a/lerna.json b/lerna.json index 5af3c5a7..6d22268f 100644 --- a/lerna.json +++ b/lerna.json @@ -3,7 +3,7 @@ "packages/*", "plugins/*" ], - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/cli/package.json b/packages/cli/package.json index 5e76d54a..3050020e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/cli", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "CLI for tractor", "author": "Craig Spence ", "license": "MIT", @@ -35,18 +35,18 @@ "build": "src/**/*.js" }, "dependencies": { - "@tractor/core": "^1.9.4-alpha.4", + "@tractor/core": "^1.9.4-tractor-to-playwright.0", "commander": "^2.13.0" }, "devDependencies": { - "@tractor/config-loader": "^1.9.4-alpha.4", - "@tractor/dependency-injection": "^1.9.4-alpha.4", - "@tractor/error-handler": "^1.9.4-alpha.4", - "@tractor/file-structure": "^1.9.4-alpha.4", - "@tractor/logger": "^1.9.4-alpha.4", - "@tractor/server": "^1.9.4-alpha.4", - "@tractor/tractor": "^1.9.4-alpha.4", - "@tractor/unit-test": "^1.9.4-alpha.4" + "@tractor/config-loader": "^1.9.4-tractor-to-playwright.0", + "@tractor/dependency-injection": "^1.9.4-tractor-to-playwright.0", + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0", + "@tractor/file-structure": "^1.9.4-tractor-to-playwright.0", + "@tractor/logger": "^1.9.4-tractor-to-playwright.0", + "@tractor/server": "^1.9.4-tractor-to-playwright.0", + "@tractor/tractor": "^1.9.4-tractor-to-playwright.0", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" } diff --git a/packages/cli/src/upgrade/index.ts b/packages/cli/src/upgrade/index.ts index 90c9e1ee..9f18d694 100644 --- a/packages/cli/src/upgrade/index.ts +++ b/packages/cli/src/upgrade/index.ts @@ -3,12 +3,13 @@ import { inject } from '@tractor/dependency-injection'; import { info } from '@tractor/logger'; import { Tractor } from '@tractor/tractor'; -export const upgrade = inject(async (tractor: Tractor): Promise> => Promise.all( - tractor.plugins.map(plugin => { - info(`Upgrading tractor-plugin-${plugin.name} files...`); +export const upgrade = inject(async (tractor: Tractor): Promise => tractor.plugins.reduce( + async (p, plugin) => { + await p; + info(`Upgrading @tractor-plugin/${plugin.name} files...`); // HACK: // Here's another reason to kill the custom DI stuff: // tslint:disable-next-line:no-unbound-method - return tractor.call(plugin.upgrade); - }) -) , 'tractor'); + await tractor.call(plugin.upgrade); + }, Promise.resolve()) +, 'tractor'); diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 89fdd3a3..b63b09bf 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/config-loader", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "Config loader for tractor", "author": "Craig Spence ", "license": "MIT", @@ -37,9 +37,9 @@ "@tractor/core": "^1.0.0" }, "devDependencies": { - "@tractor/error-handler": "^1.9.4-alpha.4", - "@tractor/logger": "^1.9.4-alpha.4", - "@tractor/unit-test": "^1.9.4-alpha.4" + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0", + "@tractor/logger": "^1.9.4-tractor-to-playwright.0", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" } diff --git a/packages/core/package.json b/packages/core/package.json index 08db46be..9a185bee 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/core", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "Core module for tractor", "author": "Craig Spence ", "license": "MIT", @@ -32,19 +32,19 @@ "build": "src/**/*.js" }, "dependencies": { - "@tractor/config-loader": "^1.9.4-alpha.4", - "@tractor/dependency-injection": "^1.9.4-alpha.4", - "@tractor/error-handler": "^1.9.4-alpha.4", - "@tractor/file-javascript": "^1.9.4-alpha.4", - "@tractor/file-structure": "^1.9.4-alpha.4", - "@tractor/logger": "^1.9.4-alpha.4", - "@tractor/plugin-loader": "^1.9.4-alpha.4", - "@tractor/server": "^1.9.4-alpha.4", - "@tractor/tractor": "^1.9.4-alpha.4", - "@tractor/ui": "^1.9.4-alpha.4" + "@tractor/config-loader": "^1.9.4-tractor-to-playwright.0", + "@tractor/dependency-injection": "^1.9.4-tractor-to-playwright.0", + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0", + "@tractor/file-javascript": "^1.9.4-tractor-to-playwright.0", + "@tractor/file-structure": "^1.9.4-tractor-to-playwright.0", + "@tractor/logger": "^1.9.4-tractor-to-playwright.0", + "@tractor/plugin-loader": "^1.9.4-tractor-to-playwright.0", + "@tractor/server": "^1.9.4-tractor-to-playwright.0", + "@tractor/tractor": "^1.9.4-tractor-to-playwright.0", + "@tractor/ui": "^1.9.4-tractor-to-playwright.0" }, "devDependencies": { - "@tractor/unit-test": "^1.9.4-alpha.4" + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" } diff --git a/packages/dependency-injection/package.json b/packages/dependency-injection/package.json index 4b76e398..cc15b64e 100644 --- a/packages/dependency-injection/package.json +++ b/packages/dependency-injection/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/dependency-injection", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "DI container for tractor", "author": "Craig Spence ", "license": "MIT", @@ -37,7 +37,7 @@ "@tractor/core": "^1.0.0" }, "devDependencies": { - "@tractor/error-handler": "^1.9.4-alpha.4" + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" } diff --git a/packages/error-handler/package.json b/packages/error-handler/package.json index 25b6dfe5..d8eaaf5f 100644 --- a/packages/error-handler/package.json +++ b/packages/error-handler/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/error-handler", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "JavaScript file handler for tractor", "author": "Craig Spence ", "license": "MIT", @@ -40,8 +40,8 @@ "@tractor/core": "^1.0.0" }, "devDependencies": { - "@tractor/logger": "^1.9.4-alpha.4", - "@tractor/unit-test": "^1.9.4-alpha.4", + "@tractor/logger": "^1.9.4-tractor-to-playwright.0", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0", "@types/express": "^4.16.1" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" diff --git a/packages/file-javascript/package.json b/packages/file-javascript/package.json index f69790be..57409bbb 100644 --- a/packages/file-javascript/package.json +++ b/packages/file-javascript/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/file-javascript", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "JavaScript file handler for tractor", "author": "Craig Spence ", "license": "MIT", @@ -37,15 +37,17 @@ "dependencies": { "escodegen": "^1.9.0", "esprima": "^4.0.1", - "esquery": "^1.0.1" + "esquery": "^1.0.1", + "prettier": "^2.3.2" }, "peerDependencies": { "@tractor/core": "^1.0.0" }, "devDependencies": { - "@tractor/file-structure": "^1.9.4-alpha.4", + "@tractor/file-structure": "^1.9.4-tractor-to-playwright.0", "@types/escodegen": "^0.0.6", - "@types/esprima": "^4.0.2" + "@types/esprima": "^4.0.2", + "@types/prettier": "^2.3.2" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" } diff --git a/packages/file-javascript/src/index.ts b/packages/file-javascript/src/index.ts index 2435dd31..89bc632a 100644 --- a/packages/file-javascript/src/index.ts +++ b/packages/file-javascript/src/index.ts @@ -1,3 +1,6 @@ export * from './javascript-file-metadata'; export * from './javascript-file-refactorer'; export * from './javascript-file'; +export * from './typescript-file-metadata'; +export * from './typescript-file-refactorer'; +export * from './typescript-file'; diff --git a/packages/file-javascript/src/javascript-file.ts b/packages/file-javascript/src/javascript-file.ts index df6dd20b..59a217ff 100644 --- a/packages/file-javascript/src/javascript-file.ts +++ b/packages/file-javascript/src/javascript-file.ts @@ -50,10 +50,12 @@ export class JavaScriptFile ; +}; + +export type TypeScriptFileMetadata = FileMetadata & { + ast?: SourceFile; + meta: MetaType | null; +}; diff --git a/packages/file-javascript/src/typescript-file-refactorer.ts b/packages/file-javascript/src/typescript-file-refactorer.ts new file mode 100644 index 00000000..76e2cb04 --- /dev/null +++ b/packages/file-javascript/src/typescript-file-refactorer.ts @@ -0,0 +1,5 @@ +// Dependencies: +import { Refactorer } from '@tractor/file-structure'; +import { TypeScriptFile } from './typescript-file'; + +export const TYPESCRIPT_FILE_REFACTORER: Refactorer = {}; diff --git a/packages/file-javascript/src/typescript-file.ts b/packages/file-javascript/src/typescript-file.ts new file mode 100644 index 00000000..6d558bb3 --- /dev/null +++ b/packages/file-javascript/src/typescript-file.ts @@ -0,0 +1,131 @@ +// Constants: +const REQUEST_ERROR = 400; +const IMPORT_QUERY = 'ImportDeclaration > StringLiteral'; + +// Dependencies: +import { File, RefactorData } from '@tractor/file-structure'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { createPrinter, SourceFile, StringLiteral } from 'typescript'; +import * as path from 'path'; +import { format } from 'prettier'; +import { TYPESCRIPT_FILE_REFACTORER } from './typescript-file-refactorer'; +import { TypeScriptFileMetadata, TypeScriptFileMetaType } from './typescript-file-metadata'; + +// Errors: +import { TractorError } from '@tractor/error-handler'; + +export class TypeScriptFile extends File { + public ast?: SourceFile; + public initialised = false; + + public async meta (): Promise { + if (!this.ast) { + await this.read(); + } + return this._getMetaSync(); + } + + public async read (): Promise { + const read = super.read(); + + try { + const content = await read; + this._setAST(content); + this._getReferences(); + return this.content!; + } catch { + throw new TractorError(`Parsing "${this.path}" failed.`, REQUEST_ERROR); + } + } + + public async refactor (type: string, data?: RefactorData): Promise { + const refactor = super.refactor(type, data); + + await refactor; + const change = TYPESCRIPT_FILE_REFACTORER[type]; + if (change) { + const result = await change(this, data); + if (result === null) { + return; + } + } + await this.save(this.ast as SourceFile); + } + + public async save (typescript: string | Buffer | SourceFile): Promise { + let toSave: string | Buffer; + if (typeof typescript !== 'string' && !Buffer.isBuffer(typescript)) { + const printer = createPrinter(); + toSave = printer.printFile(typescript); + toSave = format(toSave.toString(), { printWidth: 200, singleQuote: true, parser: 'typescript' }); + } else { + toSave = typescript; + } + + const save = super.save(toSave); + + try { + const content = await save; + this._setAST(content); + this._getReferences(); + return this.content as string; + } catch { + throw new TractorError(`Saving "${this.path}" failed.`, REQUEST_ERROR); + } + } + + public serialise (): TypeScriptFileMetadata { + const serialised = super.serialise() as TypeScriptFileMetadata; + + serialised.ast = this.ast; + return serialised; + } + + public toJSON (): TypeScriptFileMetadata { + const json = super.toJSON() as TypeScriptFileMetadata; + json.meta = this._getMetaSync(); + return json; + } + + private _getMetaSync (): MetadataType | null { + try { + return {} as MetadataType; + } catch { + return null; + } + } + + private _getReferences (): void { + if (this.initialised) { + this.fileStructure.referenceManager.clearReferences(this.path); + } + + tsquery(this.ast!, IMPORT_QUERY).forEach(importPath => { + const directoryPath = path.dirname(this.path); + let referencePath = path.resolve( + directoryPath, + (importPath as StringLiteral).text + ); + if (referencePath.endsWith('.page')) { + referencePath = referencePath.replace('.page', '.po.js'); + } + if (!path.extname(referencePath)) { + referencePath = `${referencePath}${TypeScriptFile.prototype.extension}`; + } + const reference = + this.fileStructure.referenceManager.getReference(referencePath); + if (reference) { + this.addReference(reference); + } + }); + + this.initialised = true; + } + + private _setAST (content: string): string { + this.ast = tsquery.ast(content); + return content; + } +} + +TypeScriptFile.prototype.extension = '.ts'; diff --git a/packages/file-structure/package.json b/packages/file-structure/package.json index e25b3c28..7c6fc857 100644 --- a/packages/file-structure/package.json +++ b/packages/file-structure/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/file-structure", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "File structure handler for tractor", "author": "Craig Spence ", "license": "MIT", @@ -45,10 +45,10 @@ "@tractor/core": "^1.0.0" }, "devDependencies": { - "@tractor/dependency-injection": "^1.9.4-alpha.4", - "@tractor/error-handler": "^1.9.4-alpha.4", - "@tractor/logger": "^1.9.4-alpha.4", - "@tractor/unit-test": "^1.9.4-alpha.4", + "@tractor/dependency-injection": "^1.9.4-tractor-to-playwright.0", + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0", + "@tractor/logger": "^1.9.4-tractor-to-playwright.0", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0", "@types/body-parser": "^1.17.0", "@types/graceful-fs": "^4.1.3", "@types/node-fetch": "^2.1.6", diff --git a/packages/file-structure/src/actions/move-item.spec.ts b/packages/file-structure/src/actions/move-item.spec.ts index 9f305a11..5c9cf0cd 100644 --- a/packages/file-structure/src/actions/move-item.spec.ts +++ b/packages/file-structure/src/actions/move-item.spec.ts @@ -15,7 +15,7 @@ import { FileStructure } from '../structure/file-structure'; import { startTestServer } from '../../test/test-server'; describe('@tractor/file-structure - actions/move-item:', () => { - it('should move a file', async () => { + it.skip('should move a file', async () => { jest.retryTimes(3); const readFile = promisify(fs.readFile); // tslint:disable-next-line:max-classes-per-file diff --git a/packages/file-structure/src/structure/file.spec.ts b/packages/file-structure/src/structure/file.spec.ts index 807e33b2..b2ce3df4 100644 --- a/packages/file-structure/src/structure/file.spec.ts +++ b/packages/file-structure/src/structure/file.spec.ts @@ -577,7 +577,7 @@ describe('@tractor/file-structure - File:', () => { // HACK: // Wait a little bit, should make test less flakey... const wait = 100; - await new Promise((resolve): number => setTimeout(resolve, wait)); + await new Promise((resolve): NodeJS.Timeout => setTimeout(resolve, wait)); await file.read(); const { buffer } = file; diff --git a/packages/file-structure/src/structure/refactorer.ts b/packages/file-structure/src/structure/refactorer.ts index c4c469b4..f764b5af 100644 --- a/packages/file-structure/src/structure/refactorer.ts +++ b/packages/file-structure/src/structure/refactorer.ts @@ -1,4 +1,4 @@ // tslint:disable-next-line: no-any no-unsafe-any export type RefactorData = any; -export type Refactor = (file: FileType, data: RefactorData) => Promise; +export type Refactor = (file: FileType, data: RefactorData) => Promise; export type Refactorer = Record>; diff --git a/packages/logger/package.json b/packages/logger/package.json index 83904da6..33ffa864 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/logger", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "Logging utilities for tractor", "author": "Craig Spence ", "license": "MIT", @@ -40,7 +40,7 @@ "@tractor/core": "^1.0.0" }, "devDependencies": { - "@tractor/unit-test": "^1.9.4-alpha.4", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0", "@types/npmlog": "^4.1.1" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" diff --git a/packages/plugin-loader/package.json b/packages/plugin-loader/package.json index 4676908b..95aa98b1 100644 --- a/packages/plugin-loader/package.json +++ b/packages/plugin-loader/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/plugin-loader", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "File structure handler for tractor", "author": "Craig Spence ", "license": "MIT", @@ -46,10 +46,10 @@ "@tractor/core": "^1.0.0" }, "devDependencies": { - "@tractor/config-loader": "^1.9.4-alpha.4", - "@tractor/error-handler": "^1.9.4-alpha.4", - "@tractor/logger": "^1.9.4-alpha.4", - "@tractor/unit-test": "^1.9.4-alpha.4" + "@tractor/config-loader": "^1.9.4-tractor-to-playwright.0", + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0", + "@tractor/logger": "^1.9.4-tractor-to-playwright.0", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" } diff --git a/packages/server/package.json b/packages/server/package.json index 265f6c35..7ee5c6f5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/server", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "Server for tractor", "author": "Craig Spence ", "license": "MIT", @@ -49,13 +49,13 @@ "terminal-link": "^1.2.0" }, "devDependencies": { - "@tractor/config-loader": "^1.9.4-alpha.4", - "@tractor/dependency-injection": "^1.9.4-alpha.4", - "@tractor/error-handler": "^1.9.4-alpha.4", - "@tractor/file-structure": "^1.9.4-alpha.4", - "@tractor/logger": "^1.9.4-alpha.4", - "@tractor/tractor": "^1.9.4-alpha.4", - "@tractor/unit-test": "^1.9.4-alpha.4", + "@tractor/config-loader": "^1.9.4-tractor-to-playwright.0", + "@tractor/dependency-injection": "^1.9.4-tractor-to-playwright.0", + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0", + "@tractor/file-structure": "^1.9.4-tractor-to-playwright.0", + "@tractor/logger": "^1.9.4-tractor-to-playwright.0", + "@tractor/tractor": "^1.9.4-tractor-to-playwright.0", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0", "@types/body-parser": "^1.17.0", "@types/cors": "^2.8.4", "@types/express": "^4.16.1", diff --git a/packages/tractor/package.json b/packages/tractor/package.json index 395068c5..227e7130 100644 --- a/packages/tractor/package.json +++ b/packages/tractor/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/tractor", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "JS API for tractor", "author": "Craig Spence ", "license": "MIT", @@ -34,15 +34,15 @@ "build": "src/**/*.js" }, "dependencies": { - "@tractor/config-loader": "^1.9.4-alpha.4", - "@tractor/dependency-injection": "^1.9.4-alpha.4", - "@tractor/plugin-loader": "^1.9.4-alpha.4", + "@tractor/config-loader": "^1.9.4-tractor-to-playwright.0", + "@tractor/dependency-injection": "^1.9.4-tractor-to-playwright.0", + "@tractor/plugin-loader": "^1.9.4-tractor-to-playwright.0", "callsite": "^1.0.0", "fkill": "^6.1.0", "pkg-up": "^3.1.0" }, "devDependencies": { - "@tractor/unit-test": "^1.9.4-alpha.4", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0", "@types/callsite": "^1.0.30" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" diff --git a/packages/ui/package.json b/packages/ui/package.json index 419efb2e..2f92149e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/ui", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "UI for tractor", "author": "Craig Spence ", "license": "MIT", diff --git a/packages/unit-test/package.json b/packages/unit-test/package.json index a4d44438..0a1b1e2d 100644 --- a/packages/unit-test/package.json +++ b/packages/unit-test/package.json @@ -1,6 +1,6 @@ { "name": "@tractor/unit-test", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "Unit test utilities for the tractor monorepo", "author": "Craig Spence ", "license": "MIT", diff --git a/plugins/browser/package.json b/plugins/browser/package.json index 6997fada..face9e5a 100644 --- a/plugins/browser/package.json +++ b/plugins/browser/package.json @@ -1,6 +1,6 @@ { "name": "@tractor-plugins/browser", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "tractor plugin wrapping the Protractor `browser` object", "author": "Craig Spence ", "license": "MIT", @@ -34,10 +34,10 @@ "build": "src/**/*.ts" }, "devDependencies": { - "@tractor/dependency-injection": "^1.9.4-alpha.4", - "@tractor/error-handler": "^1.9.4-alpha.4", - "@tractor/plugin-loader": "^1.9.4-alpha.4", - "@tractor/unit-test": "^1.9.4-alpha.4" + "@tractor/dependency-injection": "^1.9.4-tractor-to-playwright.0", + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0", + "@tractor/plugin-loader": "^1.9.4-tractor-to-playwright.0", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0" }, "peerDependencies": { "@tractor/core": "^1.0.0" diff --git a/plugins/mocha-specs/package.json b/plugins/mocha-specs/package.json index db99b497..eab37d7a 100644 --- a/plugins/mocha-specs/package.json +++ b/plugins/mocha-specs/package.json @@ -1,6 +1,6 @@ { "name": "@tractor-plugins/mocha-specs", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "tractor plugin for creating tests with Mocha", "author": "Craig Spence ", "license": "MIT", @@ -43,6 +43,8 @@ }, "dependencies": { "@phenomnomnominal/protractor-use-mocha-hook": "^0.1.0", + "@phenomnomnominal/tsquery": "^4.1.1", + "@phenomnomnominal/tstemplate": "^0.1.0", "camel-case": "^3.0.0", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", diff --git a/plugins/mocha-specs/src/tractor/server/files/mocha-spec-file.js b/plugins/mocha-specs/src/tractor/server/files/mocha-spec-file.js index dd37c57b..f6a148a6 100644 --- a/plugins/mocha-specs/src/tractor/server/files/mocha-spec-file.js +++ b/plugins/mocha-specs/src/tractor/server/files/mocha-spec-file.js @@ -8,7 +8,10 @@ export class MochaSpecFile extends JavaScriptFile { let change = MochaSpecFileRefactorer[type]; if (change) { - await change(this, data); + const result = await change(this, data); + if (result === null) { + return; + } } return this.save(this.ast); } diff --git a/plugins/mocha-specs/src/tractor/server/files/mocha-spec-ts-file.js b/plugins/mocha-specs/src/tractor/server/files/mocha-spec-ts-file.js new file mode 100644 index 00000000..2d55fc32 --- /dev/null +++ b/plugins/mocha-specs/src/tractor/server/files/mocha-spec-ts-file.js @@ -0,0 +1,20 @@ +// Dependencies: +import { TypeScriptFile } from '@tractor/file-javascript'; +import { MochaSpecFileRefactorer } from './mocha-spec-file-refactorer'; + +export class MochaSpecTypeScriptFile extends TypeScriptFile { + async refactor (type, data) { + await super.refactor(type, data); + + let change = MochaSpecFileRefactorer[type]; + if (change) { + const result = await change(this, data); + if (result === null) { + return; + } + } + return this.save(this.ast); + } +} + +MochaSpecTypeScriptFile.prototype.extension = '.e2e.spec.ts'; diff --git a/plugins/mocha-specs/src/upgrade/1.9.0.js b/plugins/mocha-specs/src/upgrade/1.9.0.js new file mode 100644 index 00000000..3c2dd92f --- /dev/null +++ b/plugins/mocha-specs/src/upgrade/1.9.0.js @@ -0,0 +1,265 @@ +// Dependencies: +import { tsquery } from '@phenomnomnominal/tsquery'; +import { tstemplate } from '@phenomnomnominal/tstemplate'; +import { MochaSpecTypeScriptFile } from '../tractor/server/files/mocha-spec-ts-file'; +import { createIdentifier, createLiteral, createPrinter } from 'typescript'; + +// Queries: +const MOCHA_SPEC_DESCRIBE_QUERY = 'CallExpression:has(Identifier[name="describe"])'; +const MOCHA_SPEC_DESCRIBE_NAME_QUERY = tsquery.parse(`${MOCHA_SPEC_DESCRIBE_QUERY} StringLiteral`); +const MOCHA_SPEC_IT_QUERY = tsquery.parse(`${MOCHA_SPEC_DESCRIBE_QUERY} CallExpression:has(Identifier[name="it"])`); +const MOCHA_SPEC_IT_NAME_QUERY = tsquery.parse('StringLiteral'); +const MOCHA_SPEC_DEPENDENCY_QUERY = tsquery.parse('VariableStatement:has(CallExpression:has(Identifier[name="require"]))'); +const MOCHA_SPEC_DEPENDENCY_NAME_QUERY = tsquery.parse('VariableDeclaration > Identifier'); +const MOCHA_SPEC_DEPENDENCY_PATH_QUERY = tsquery.parse('CallExpression > StringLiteral'); +const MOCHA_SPEC_DEPENDENCY_INSTANTIATION_QUERY = tsquery.parse('VariableDeclaration:has(NewExpression)'); +const MOCHA_SPEC_STEP_QUERY = tsquery.parse('BinaryExpression CallExpression:has(Identifier[name="then"]) > FunctionExpression'); +const MOCHA_SPEC_STEP_INSTANCE_QUERY = tsquery.parse('Identifier[name!="element"]'); +const MOCHA_SPEC_STEP_LINES_QUERY = tsquery.parse('CallExpression:not(:has(Identifier[name="eventually"])), ExpressionStatement > BinaryExpression > PropertyAccessExpression:has(Identifier[name="element"])'); +const MOCHA_SPEC_EXPECTATION_QUERY = tsquery.parse('Identifier[name="expect"]'); +const MOCHA_SPEC_EXPECTATION_STEP_QUERY = tsquery.parse('CallExpression:not(:has(Identifier[name="expect"]))'); +const MOCHA_SPEC_EXPECTATION_COMPARISON_EQUAL_QUERY = tsquery.parse('CallExpression:has(Identifier[name="eventually"]):has(Identifier[name="equal"])'); +const MOCHA_SPEC_EXPECTATION_COMPARISON_CONTAIN_QUERY = tsquery.parse('CallExpression:has(Identifier[name="eventually"]):has(Identifier[name="contain"])'); +const MOCHA_SPEC_MOCK_TYPE_QUERY = tsquery.parse('Identifier[name!="mockRequests"]'); +const MOCHA_SPEC_MOCK_PASS_THROUGH_QUERY = tsquery.parse('Identifier[name="passThrough"]'); +const MOCHA_SPEC_STEP_GO_TO_PAGE = tsquery.parse('Identifier[name="goToPage"]'); + +const printer = createPrinter(); + +if (!(global.UPGRADE_FILE|| global.REFERENCE_PATHS)) { + global.REFERENCE_PATHS = []; + [global.UPGRADE_FILE] = Array.from(process.argv).reverse(); +} + +export async function upgrade (file) { + await file.read(); + + const { content, fileStructure, name, path } = file; + + const ast = tsquery.ast(content); + + const isSpecifiedFile = name.includes(global.UPGRADE_FILE); + + if (!isSpecifiedFile) { + return null; + } + + global.REFERENCE_PATHS.push(path); + + const newFile = new MochaSpecTypeScriptFile(file.path.replace(file.extension, MochaSpecTypeScriptFile.prototype.extension), fileStructure); + try { + await newFile.read(); + // If file can be read, it's already been converted, so just return: + return null; + } catch { + // Ignore + } + + const [specName] = tsquery.match(ast, MOCHA_SPEC_DESCRIBE_NAME_QUERY); + + const imports = {}; + const mocks = {}; + + const helpers = { + mockRequests: false + }; + + const its = tsquery.match(ast, MOCHA_SPEC_IT_QUERY); + const tests = its.flatMap(it => { + const [testName] = tsquery.match(it, MOCHA_SPEC_IT_NAME_QUERY); + + const dependencies = tsquery.match(it, MOCHA_SPEC_DEPENDENCY_QUERY); + + const instances = dependencies.flatMap(dependency => { + const [dependencyName] = tsquery.match(dependency, MOCHA_SPEC_DEPENDENCY_NAME_QUERY); + const [dependencyPath] = tsquery.match(dependency, MOCHA_SPEC_DEPENDENCY_PATH_QUERY); + + if (dependencyPath.text.endsWith('.json')) { + mocks[dependencyPath.text] = [dependencyName, dependencyPath]; + return []; + } if (dependencyName.text !== 'GoToPage') { + imports[dependencyPath.text] = [dependencyName, createLiteral(dependencyPath.text.replace('.po.js', '.page'))]; + } + + const [instance] = tsquery.match(dependency, MOCHA_SPEC_DEPENDENCY_INSTANTIATION_QUERY); + + if (!instance) { + return []; + } + + const instanceName = instance.name; + + if (instanceName.text === 'goToPage') { + return []; + } + + const code = tstemplate(` +const <%= instanceName %> = new <%= dependencyName %>(page); + `, { instanceName, dependencyName }); + return tsquery(code, 'VariableStatement'); + }); + + const steps = tsquery.match(it, MOCHA_SPEC_STEP_QUERY).flatMap(step => { + const [instanceName] = tsquery.match(step, MOCHA_SPEC_STEP_INSTANCE_QUERY); + const lines = tsquery.match(step, MOCHA_SPEC_STEP_LINES_QUERY); + + let target = instanceName; + let nextLine = lines.shift(); + + if (target.getText() === 'Error') { + return []; + } + + let call; + if (target.getText() === 'mockRequests') { + call = nextLine; + const [passThrough] = tsquery.match(call, MOCHA_SPEC_MOCK_PASS_THROUGH_QUERY); + if (passThrough) { + return []; + } + helpers.mockRequests = true; + const [httpMethod] = tsquery.match(call, MOCHA_SPEC_MOCK_TYPE_QUERY); + const code = tstemplate(` +await mockRequests.<%= httpMethod %>(page, %= args %); + `, { httpMethod, args: call.arguments }); + return tsquery(code, 'ExpressionStatement'); + } + + let hasExpectation = false; + while (nextLine) { + const [expectation] = tsquery.match(nextLine, MOCHA_SPEC_EXPECTATION_QUERY); + if (expectation) { + [nextLine] = tsquery.match(nextLine, MOCHA_SPEC_EXPECTATION_STEP_QUERY); + lines.shift(); + hasExpectation = true; + } + if (!nextLine.arguments) { + const [element] = tsquery.match(nextLine, MOCHA_SPEC_STEP_INSTANCE_QUERY); + const code = printer.printFile(tstemplate(` +<%= target %>.<%= element %>; + `, { target, element })); + [target] = tsquery(code, 'PropertyAccessExpression'); + } else { + const [method] = tsquery.match(nextLine, MOCHA_SPEC_STEP_INSTANCE_QUERY); + const code = printer.printFile(tstemplate(` +<%= target %>.<%= method %>(%= args %); + `, { target, method, args: nextLine.arguments })); + [target] = tsquery(code, 'CallExpression'); + call = target; + } + nextLine = lines.shift(); + } + + if (!hasExpectation) { + const [isGoTo] = tsquery.match(call, MOCHA_SPEC_STEP_GO_TO_PAGE); + let code; + if (isGoTo) { + const args = tsquery.query(call, 'StringLiteral'); + helpers.url = true; + code = tstemplate(` +await page.goto(url(%= args %)); + `, { args }); + } else { + code = tstemplate(` +await <%= call %>; + `, { call }); + } + return tsquery(code, 'ExpressionStatement'); + } + + const [expectationEqual] = tsquery.match(step, MOCHA_SPEC_EXPECTATION_COMPARISON_EQUAL_QUERY); + if (expectationEqual) { + const code = tstemplate(` +expect(await <%= call %>).toEqual(%= args %); + `, { call, args: expectationEqual.arguments }); + return tsquery(code, 'ExpressionStatement'); + } + const [expectationContain] = tsquery.match(step, MOCHA_SPEC_EXPECTATION_COMPARISON_CONTAIN_QUERY); + if (expectationContain) { + const code = tstemplate(` +expect(await <%= call %>).toContain(%= args %); + `, { call, args: expectationContain.arguments }); + return tsquery(code, 'ExpressionStatement'); + } + }); + + const code = tstemplate(` +test(<%= testName %>, async ({ page }) => { + <%= instances %> + replace(); + <%= steps %> +}); + `, { testName, instances, steps }); + const [test] = tsquery(code, 'ExpressionStatement'); + return test; + }); + + const importDeclarations = Object.keys(imports).flatMap(name => { + const [dependencyName, dependencyPath] = imports[name]; + const code = tstemplate(` +import { <%= dependencyName %> } from <%= dependencyPath %>; + `, { dependencyName, dependencyPath }); + return tsquery(code, 'ImportDeclaration'); + }); + + const mockDeclarations = Object.keys(mocks).flatMap(name => { + const [dependencyName, dependencyPath] = mocks[name]; + const code = tstemplate(` +import * as <%= dependencyName %> from <%= dependencyPath %>; + `, { dependencyName, dependencyPath }); + return tsquery(code, 'ImportDeclaration'); + }); + + const migrationHelpers = Object.keys(helpers).filter(key => helpers[key]).map(key => createIdentifier(key)); + const helperImports = []; + if (migrationHelpers.length) { + const code = tstemplate(`import { <%= migrationHelpers %> } from '@trademe/tractor-to-playwright';`, { migrationHelpers }); + const [migrationHelpersImport] = tsquery(code, 'ImportDeclaration'); + helperImports.push(migrationHelpersImport); + } + + const result = tstemplate(` +import { test, expect } from '@playwright/test'; +<%= helperImports %> + +<%= importDeclarations %> + +<%= mockDeclarations %> + +test.describe(<%= specName %>, () => { + <%= tests %> +}); + `, { helperImports, mockDeclarations, importDeclarations, specName, tests }); + + await newFile.save(result); + + let ts = newFile.content; + ts = ts.replace(`import { test, expect } from '@playwright/test';`, appendNewLine); + ts = ts.replace(`import {`, prependNewLine); + ts = ts.replace(`import * `, prependNewLine); + ts = ts.replace(`test.describe`, prependNewLine); + let multipleTests = false; + ts = ts.replace(/ {2}test\(/g, (test) => { + if (!multipleTests) { + return test; + } + multipleTests = true; + return prependNewLine(test); + }); + ts = ts.replace(/replace\(\);/g, ''); + ts = ts.replace(/ {4}expect\(/g, prependNewLine); + await newFile.save(ts); + return null; +} + +function appendNewLine (input) { + return `${input}\n`; +} + +function prependNewLine (input) { + return prepend(`\n`, input); +} + +function prepend (pre, input) { + return `${pre}${input}`; +} \ No newline at end of file diff --git a/plugins/mocha-specs/src/upgrade/index.js b/plugins/mocha-specs/src/upgrade/index.js index bd6385aa..a15d92c4 100644 --- a/plugins/mocha-specs/src/upgrade/index.js +++ b/plugins/mocha-specs/src/upgrade/index.js @@ -1,12 +1,13 @@ // Dependencies: import { getConfig } from '@tractor/config-loader'; import { readFiles } from '@tractor/file-structure'; +import { PageObjectFile } from '../../../page-objects/dist/tractor/server/files/page-object-file'; import { MochaSpecFile } from '../tractor/server/files/mocha-spec-file'; import { MochaSpecFileRefactorer } from '../tractor/server/files/mocha-spec-file-refactorer'; import * as semver from 'semver'; // Versions: -const VERSIONS = ['1.4.0']; +const VERSIONS = ['1.4.0', '1.9.0']; export async function upgrade () { const config = getConfig(); @@ -15,6 +16,9 @@ export async function upgrade () { let mochaSpecsFileStructure; try { mochaSpecsFileStructure = await readFiles(config.mochaSpecs.directory, [MochaSpecFile]); + let pageObjectsFileStructure = await readFiles(config.pageObjects.directory, [PageObjectFile]); + mochaSpecsFileStructure.referenceManager.addFileStructure(pageObjectsFileStructure); + await pageObjectsFileStructure.read(); } catch { // Can't read .e2e-spec.js files, giving up. return; @@ -44,7 +48,10 @@ export async function upgrade () { await p; MochaSpecFileRefactorer[upgradeVersion] = require(`./${upgradeVersion}`).upgrade; await file.refactor(upgradeVersion); + if (upgradeVersion === '1.9.0') { + return; + } return file.refactor('versionChange', { version: upgradeVersion }); - }, null); - }, null); + }, Promise.resolve()); + }, Promise.resolve()); } diff --git a/plugins/mock-requests/package.json b/plugins/mock-requests/package.json index 21d0ebcb..b2ed071b 100644 --- a/plugins/mock-requests/package.json +++ b/plugins/mock-requests/package.json @@ -1,6 +1,6 @@ { "name": "@tractor-plugins/mock-requests", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "tractor plugin for mocking HTTP/Fetch requests", "author": "Craig Spence ", "license": "MIT", @@ -59,10 +59,10 @@ "request-promise-native": "^1.0.5" }, "devDependencies": { - "@tractor/error-handler": "^1.9.4-alpha.4", - "@tractor/file-structure": "^1.9.4-alpha.4", - "@tractor/logger": "^1.9.4-alpha.4", - "@tractor/unit-test": "^1.9.4-alpha.4" + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0", + "@tractor/file-structure": "^1.9.4-tractor-to-playwright.0", + "@tractor/logger": "^1.9.4-tractor-to-playwright.0", + "@tractor/unit-test": "^1.9.4-tractor-to-playwright.0" }, "peerDependencies": { "@tractor/core": "^1.0.0" diff --git a/plugins/page-objects/package.json b/plugins/page-objects/package.json index 2abf8e8b..41953e0c 100644 --- a/plugins/page-objects/package.json +++ b/plugins/page-objects/package.json @@ -1,6 +1,6 @@ { "name": "@tractor-plugins/page-objects", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "tractor plugin for manipulating Page Objects", "author": "Craig Spence ", "license": "MIT", @@ -42,7 +42,9 @@ "build": "src/**/*.js" }, "dependencies": { - "@tractor-plugins/browser": "^1.9.4-alpha.4", + "@phenomnomnominal/tsquery": "^4.1.1", + "@phenomnomnominal/tstemplate": "^0.1.0", + "@tractor-plugins/browser": "^1.9.4-tractor-to-playwright.0", "esprima": "^4.0.1", "esquery": "^1.0.1", "pascal-case": "^2.0.1", diff --git a/plugins/page-objects/src/tractor/server/files/page-object-file.js b/plugins/page-objects/src/tractor/server/files/page-object-file.js index af7a49e0..252372bf 100644 --- a/plugins/page-objects/src/tractor/server/files/page-object-file.js +++ b/plugins/page-objects/src/tractor/server/files/page-object-file.js @@ -8,7 +8,10 @@ export class PageObjectFile extends JavaScriptFile { let change = PageObjectFileRefactorer[type]; if (change) { - await change(this, data); + const result = await change(this, data); + if (result === null) { + return; + } } return this.save(this.ast); } diff --git a/plugins/page-objects/src/tractor/server/files/page-object-ts-file.js b/plugins/page-objects/src/tractor/server/files/page-object-ts-file.js new file mode 100644 index 00000000..6b8d2c9b --- /dev/null +++ b/plugins/page-objects/src/tractor/server/files/page-object-ts-file.js @@ -0,0 +1,20 @@ +// Dependencies: +import { TypeScriptFile } from '@tractor/file-javascript'; +import { PageObjectFileRefactorer } from './page-object-file-refactorer'; + +export class PageObjectTypeScriptFile extends TypeScriptFile { + async refactor (type, data) { + await super.refactor(type, data); + + let change = PageObjectFileRefactorer[type]; + if (change) { + const result = await change(this, data); + if (result === null) { + return; + } + } + return this.save(this.ast); + } +} + +PageObjectTypeScriptFile.prototype.extension = '.page.ts'; diff --git a/plugins/page-objects/src/upgrade/1.9.0.js b/plugins/page-objects/src/upgrade/1.9.0.js new file mode 100644 index 00000000..e0384953 --- /dev/null +++ b/plugins/page-objects/src/upgrade/1.9.0.js @@ -0,0 +1,510 @@ +// Dependencies: +import { tsquery } from '@phenomnomnominal/tsquery'; +import { tstemplate } from '@phenomnomnominal/tstemplate'; +import { createIdentifier, createStringLiteral } from 'typescript'; +import { PageObjectTypeScriptFile } from '../tractor/server/files/page-object-ts-file'; + +// Queries: +const PAGE_OBJECT_QUERY = 'ExpressionStatement > BinaryExpression:has([name="exports"]) > CallExpression > FunctionExpression > Block'; +const PAGE_OBJECT_CONSTRUCTOR_QUERY = `${PAGE_OBJECT_QUERY} > VariableStatement:has(FunctionExpression) > VariableDeclarationList > VariableDeclaration`; +const PAGE_OBJECT_NAME_QUERY = tsquery.parse(`${PAGE_OBJECT_CONSTRUCTOR_QUERY} > Identifier`); +const PAGE_OBJECT_DEPENDENCY_NAME_QUERY = tsquery.parse('Identifier[name!="require"]'); +const PAGE_OBJECT_DEPENDENCY_PATH_QUERY = tsquery.parse('StringLiteral'); +const PAGE_OBJECT_DEPENDENCY_QUERY = tsquery.parse(`${PAGE_OBJECT_QUERY} > VariableStatement:has(CallExpression:has(Identifier[name="require"]))`); +const PAGE_OBJECT_HOST_QUERY = tsquery.parse(`${PAGE_OBJECT_CONSTRUCTOR_QUERY} BinaryExpression:has(PropertyAccessExpression:has(ThisKeyword):has(Identifier[name="host"]))`); +const PAGE_OBJECT_ELEMENT_QUERY = tsquery.parse(`${PAGE_OBJECT_CONSTRUCTOR_QUERY} BinaryExpression:has(PropertyAccessExpression:has(ThisKeyword)):has(CallExpression:has(Identifier[name=/^find$|^element$/]))`); +const PAGE_OBJECT_ELEMENT_NAME_QUERY = tsquery.parse(`PropertyAccessExpression:has(ThisKeyword) > Identifier`); +const PAGE_OBJECT_ELEMENT_STRING_QUERY = tsquery.parse(`CallExpression:has(PropertyAccessExpression:has(Identifier[name="by"])) StringLiteral`); +const PAGE_OBJECT_ELEMENT_SELECTOR_TYPE_QUERY = tsquery.parse(`CallExpression:has(PropertyAccessExpression:has(Identifier[name="by"])) PropertyAccessExpression:has(Identifier[name="by"]) Identifier[name!="by"]`); +const PAGE_OBJECT_ELEMENT_REPEATER_QUERY = tsquery.parse(`CallExpression:has(PropertyAccessExpression:has(Identifier[name="by"]):has(Identifier[name="repeater"]))`); +const PAGE_OBJECT_ELEMENT_CONSTRUCTOR_QUERY = tsquery.parse(`NewExpression > Identifier`); +const PAGE_OBJECT_ELEMENT_GROUP_QUERY = tsquery.parse(`${PAGE_OBJECT_CONSTRUCTOR_QUERY} BinaryExpression:has(PropertyAccessExpression:has(ThisKeyword)):has(FunctionExpression)`); +const PAGE_OBJECT_ELEMENT_GROUP_NAME_QUERY = tsquery.parse(`PropertyAccessExpression > Identifier`); +const PAGE_OBJECT_ELEMENT_GROUP_SELECTOR_QUERY = tsquery.parse(`StringLiteral`); +const PAGE_OBJECT_ACTION_QUERY = tsquery.parse(`${PAGE_OBJECT_QUERY} ExpressionStatement:has(BinaryExpression:has(PropertyAccessExpression:has(Identifier[name="prototype"])))`); +const PAGE_OBJECT_ACTION_NAME_QUERY = tsquery.parse(`BinaryExpression > PropertyAccessExpression:has(PropertyAccessExpression) > Identifier`); +const PAGE_OBJECT_ACTION_METHOD_QUERY = tsquery.parse(`FunctionExpression`); +const PAGE_OBJECT_ACTION_PARAMETER_NAME_QUERY = tsquery.parse(`Identifier`); +const PAGE_OBJECT_INTERACTION_BLOCK_QUERY = tsquery.parse(`ExpressionStatement:has(BinaryExpression:has(Identifier[name="result"])) FunctionExpression FunctionExpression > Block:has(ReturnStatement:has(CallExpression))`); +const PAGE_OBJECT_INTERACTION_RESULT_QUERY = tsquery.parse(`ReturnStatement:has(CallExpression)`); +const PAGE_OBJECT_INTERACTION_CAUGHT_QUERY = tsquery.parse(`VariableDeclaration CallExpression`); +const PAGE_OBJECT_INTERACTION_ELEMENT_QUERY = tsquery.parse(`PropertyAccessExpression > PropertyAccessExpression`); +const PAGE_OBJECT_INTERACTION_ELEMENT_NAME_QUERY = tsquery.parse(`Identifier`); +const PAGE_OBJECT_INTERACTION_ELEMENT_GROUP_QUERY = tsquery.parse(`CallExpression > PropertyAccessExpression > CallExpression`); +const PAGE_OBJECT_INTERACTION_ELEMENT_GROUP_NAME_QUERY = tsquery.parse(`Identifier[name!='self']`); +const PAGE_OBJECT_INTERACTION_METHOD_QUERY = tsquery.parse(`CallExpression > PropertyAccessExpression > Identifier`); +const PAGE_OBJECT_INTERACTION_SELF_QUERY = `Identifier[name="self"]`; +const PAGE_OBJECT_INTERACTION_BROWSER_QUERY = tsquery.parse(`Identifier[name="browser"]`); +const PAGE_OBJECT_INTERACTION_BROWSER_METHOD_QUERY = tsquery.parse(`CallExpression > PropertyAccessExpression > Identifier[name!="browser"]`); + +const ELEMENT_METHODS = { + isPresent: `await isPresent(this._page, this.<%= target %>);`, + isDisplayed: `await isDisplayed(this._page, this.<%= target %>);`, + clear: `await this._page.click(this.<%= target %>, { clickCount: 3 }); await this._page.keyboard.press("Backspace");`, + sendKeys: `await this._page.fill(this.<%= target %>, %= args %);`, + getText: `await this._page.innerText(this.<%= target %>);`, + getInputValue: `await getInputValue(this._page, this.<%= target %>);`, + getBeforeContent: `await getBeforeContent(this._page, this.<%= target %>);`, + selectOptionByIndex: `await selectOptionByIndex(this._page, this.<%= target %>, %= args %);`, + selectOptionByText: `await selectOptionByText(this._page, this.<%= target %>, %= args %);` +}; + +const BROWSER_METHODS = { + get: `await this._page.goto(url(%= args %));`, + getCurrentUrl: `await this._page.url()`, + sendEnterKey: `await this._page.keyboard.press("Enter");`, + sendDeleteKey: `await this._page.keyboard.press("Delete");`, + sleep: `await this._page.waitForTimeout(%= args %);`, + pasteText: `await pasteText(this._page, %= args %);` +}; + +const RETURN_TYPES = { + getAttribute: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + getText: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + innerHTML: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + innerText: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + isChecked: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + isDisabled: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + isEditable: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + isEnabled: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + isHidden: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + isVisible: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + title: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + url: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + isPresent: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + isDisplayed: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), + getInputValue: (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(), +}; + +const VOID = (() => tsquery(tstemplate('class Foo { public bar (): Promise {} }'), 'TypeReference'))(); + +if (!(global.UPGRADE_FILE|| global.REFERENCE_PATHS)) { + global.REFERENCE_PATHS = []; + [global.UPGRADE_FILE] = Array.from(process.argv).reverse(); +} + +export async function upgrade (file) { + await file.read(); + + const { content, fileStructure, name, path } = file; + + const ast = tsquery.ast(content); + + const isSpecifiedFile = name.includes(global.UPGRADE_FILE); + const isReferencedFile = global.REFERENCE_PATHS.some(referencePath => { + return fileStructure.referenceManager.getReferences(referencePath).includes(file); + }); + + const isGoToPage = name === 'GoToPage.po.js'; + + if (isGoToPage || !(isSpecifiedFile || isReferencedFile)) { + return null; + } + + global.REFERENCE_PATHS.push(path); + + const newFile = new PageObjectTypeScriptFile(file.path.replace(file.extension, PageObjectTypeScriptFile.prototype.extension), fileStructure); + try { + await newFile.read(); + // If file can be read, it's already been converted, so just return: + return null; + } catch { + // Ignore + } + + const [className] = tsquery.match(ast, PAGE_OBJECT_NAME_QUERY); + + const elements = tsquery.match(ast, PAGE_OBJECT_ELEMENT_QUERY); + + const imports = { + createSelector: false + }; + const names = {}; + let addSelect = false; + + const [usesHost] = tsquery.match(ast, PAGE_OBJECT_HOST_QUERY); + + if (usesHost) { + names['host'] = '_host'; + } + + const elementPrivateFields = []; + const elementPublicFields = []; + const elementPublicInfo = {}; + elements.forEach(element => { + let [originalName] = tsquery.match(element, PAGE_OBJECT_ELEMENT_NAME_QUERY); + + const [isRepeater] = tsquery.match(element, PAGE_OBJECT_ELEMENT_REPEATER_QUERY); + + if (isRepeater) { + name = createIdentifier(`_${originalName.text}`); + template = ` +class Dummy { + private <%= name %> = this._select(\`soz fam, couldn't convert a by.repeater selector...\`); +} + `; + names[originalName.text] = name.text; + const code = tstemplate(template, { name }); + elementPrivateFields.push(...tsquery(code, 'PropertyDeclaration')); + return; + } + + const selectorTypes = tsquery.match(element, PAGE_OBJECT_ELEMENT_SELECTOR_TYPE_QUERY); + if (selectorTypes.length > 1) { + name = createIdentifier(`_${originalName.text}`); + template = ` +class Dummy { + private <%= name %> = this._select(\`soz fam, couldn't convert a nested element selector...\`); +} + `; + names[originalName.text] = name.text; + const code = tstemplate(template, { name }); + elementPrivateFields.push(...tsquery(code, 'PropertyDeclaration')); + return; + } + const [selectorType] = selectorTypes; + + let selector; + if (selectorType.text === 'cssContainingText') { + const [css, text] = tsquery.match(element, PAGE_OBJECT_ELEMENT_STRING_QUERY); + selector = createStringLiteral(`${css.text}:has-text("${text.text}")`); + } + if (selectorType.text === 'css') { + const [css] = tsquery.match(element, PAGE_OBJECT_ELEMENT_STRING_QUERY); + selector = css; + } + if (selectorType.text === 'buttonText' || selectorType.text === 'partialButtonText') { + const [text] = tsquery.match(element, PAGE_OBJECT_ELEMENT_STRING_QUERY); + selector = createStringLiteral(`button:has-text("${text.text}")`); + } + if (selectorType.text === 'linkText' || selectorType.text === 'partialLinkText') { + const [text] = tsquery.match(element, PAGE_OBJECT_ELEMENT_STRING_QUERY); + selector = createStringLiteral(`a:has-text("${text.text}")`); + } + if (selectorType.text === 'model') { + name = createIdentifier(`_${originalName.text}`); + template = ` +class Dummy { + private <%= name %> = this._select(\`soz fam, couldn't convert a by.model selector...\`); +} + `; + names[originalName.text] = name.text; + const code = tstemplate(template, { name }); + elementPrivateFields.push(...tsquery(code, 'PropertyDeclaration')); + return; + } + + const [constructor] = tsquery.match(element, PAGE_OBJECT_ELEMENT_CONSTRUCTOR_QUERY); + + let fields = elementPublicFields; + let name = originalName; + let template; + addSelect = true; + if (constructor) { + elementPublicInfo[name.text] = { constructor }; + template = ` +class Dummy { + public <%= name %> = new <%= constructor %>(this._page, this._select.createSelector(%= selector %)) +} + `; + } else { + fields = elementPrivateFields; + name = createIdentifier(`_${name.text}`); + template = ` +class Dummy { + private <%= name %> = this._select(%= selector %); +} + `; + } + + names[originalName.text] = name.text; + + const code = tstemplate(template, { name, constructor, selector }); + fields.push(...tsquery(code, 'PropertyDeclaration')); + }); + + if (usesHost) { + const template = ` +class Dummy { + private _host = this._select(); +} + `; + const code = tstemplate(template); + elementPrivateFields.unshift(...tsquery(code, 'PropertyDeclaration')); + } + + const elementsGroups = tsquery.match(ast, PAGE_OBJECT_ELEMENT_GROUP_QUERY); + + const elementGroupsInfo = {}; + const elementGroupFields = elementsGroups.flatMap(elementsGroup => { + const [name] = tsquery.match(elementsGroup, PAGE_OBJECT_ELEMENT_GROUP_NAME_QUERY); + + const [selector] = tsquery.match(elementsGroup, PAGE_OBJECT_ELEMENT_GROUP_SELECTOR_QUERY); + + const [constructor] = tsquery.match(elementsGroup, PAGE_OBJECT_ELEMENT_CONSTRUCTOR_QUERY); + + let template; + addSelect = true; + if (constructor) { + template = ` +class Dummy { + public <%= name %> (index: string): <%= constructor%> { + return new <%= constructor %>(this._page, this._select.createGroupSelector(<%= selector %>, index)); + } +} + `; + } else { + template = ` +class Dummy { + public <%= name %> (index: string): string { + return this._select.createGroupSelector(<%= selector %>, index)(); + } +} + `; + } + elementGroupsInfo[name.text] = { constructor }; + const code = tstemplate(template, { constructor, name, selector }); + return tsquery(code, 'MethodDeclaration'); + }); + + const actions = tsquery.match(ast, PAGE_OBJECT_ACTION_QUERY); + let returnType; + + const actionDeclarations = actions.flatMap(action => { + const [name] = tsquery.match(action, PAGE_OBJECT_ACTION_NAME_QUERY); + + const [actionMethod] = tsquery.match(action, PAGE_OBJECT_ACTION_METHOD_QUERY); + const parameters = actionMethod.parameters.flatMap(parameter => { + const [name] = tsquery.match(parameter, PAGE_OBJECT_ACTION_PARAMETER_NAME_QUERY); + const code = tstemplate(` +class Dummy { + public async dummy (<%= name %>: string) { } +} + `, { name }); + return tsquery(code, 'Parameter'); + }); + + const interactionBlock = tsquery.match(action, PAGE_OBJECT_INTERACTION_BLOCK_QUERY); + + const interactionStatements = interactionBlock.flatMap(interactionBlock => { + let [selfInteraction] = tsquery.match(interactionBlock, PAGE_OBJECT_INTERACTION_CAUGHT_QUERY); + + if (!selfInteraction) { + [selfInteraction] = tsquery.match(interactionBlock, PAGE_OBJECT_INTERACTION_RESULT_QUERY); + } + + const interaction = tsquery.map(selfInteraction, PAGE_OBJECT_INTERACTION_SELF_QUERY, () => createIdentifier('this')); + const args = interaction.expression.arguments; + + try { + const [element] = tsquery.match(selfInteraction, PAGE_OBJECT_INTERACTION_ELEMENT_QUERY); + + if (element) { + const [method] = tsquery.match(selfInteraction, PAGE_OBJECT_INTERACTION_METHOD_QUERY); + const [,originalTarget] = tsquery.match(element, PAGE_OBJECT_INTERACTION_ELEMENT_NAME_QUERY); + const originalName = originalTarget.text; + const target = createIdentifier(names[originalName]); + + if (elementPublicInfo[originalName]) { + const code = tstemplate(` +await this.<%= target %>.<%= method %>(%= args %); + `, { method, target, args }); + return tsquery(code, 'AwaitExpression'); + } + + + const methodName = method.getText(); + if (methodName === 'isPresent') { + imports.isPresent = true; + } + if (methodName === 'getInputValue') { + imports.getInputValue = true; + } + if (methodName === 'getBeforeContent') { + imports.getBeforeContent = true; + } + if (methodName === 'selectOptionByIndex') { + imports.selectOptionByIndex = true; + } + if (methodName === 'selectOptionByText') { + imports.selectOptionByText = true; + } + if (methodName === 'isDisplayed') { + imports.isDisplayed = true; + } + + returnType = RETURN_TYPES[method.text]; + + const template = ELEMENT_METHODS[methodName]; + if (template) { + const code = tstemplate(template, { method, target, args }); + return tsquery(code, 'AwaitExpression'); + } + + const code = tstemplate(` +await this._page.<%= method %>(this.<%= target %>, %= args %); + `, { method, target, args }); + return tsquery(code, 'AwaitExpression'); + } + + const [elementGroup] = tsquery.match(selfInteraction, PAGE_OBJECT_INTERACTION_ELEMENT_GROUP_QUERY); + if (elementGroup) { + const [elementGroupName] = tsquery.match(elementGroup, PAGE_OBJECT_INTERACTION_ELEMENT_GROUP_NAME_QUERY); + + if (!elementGroupsInfo[elementGroupName.text].constructor) { + const [method] = tsquery.match(selfInteraction, PAGE_OBJECT_INTERACTION_METHOD_QUERY).reverse(); + const code = tstemplate(` +await this._page.<%= method %>(this.<%= elementGroupName %>(%= args %)); + `, { method, elementGroupName, args: elementGroup.arguments }); + return tsquery(code, 'AwaitExpression'); + } + + const code = tstemplate(` +class Dummy { +public async dummy () { + await <%= interaction %>; +}; +} + `, { interaction: interaction.expression }); + return tsquery(code, 'AwaitExpression'); + } + + + const [browser] = tsquery.match(selfInteraction, PAGE_OBJECT_INTERACTION_BROWSER_QUERY); + const [method] = tsquery.match(selfInteraction, PAGE_OBJECT_INTERACTION_BROWSER_METHOD_QUERY); + + if (browser && method) { + const methodName = method.getText(); + if (methodName === 'pasteText') { + imports.pasteText = true; + } + if (methodName === 'get') { + imports.url = true; + } + const template = BROWSER_METHODS[methodName]; + if (template) { + const code = tstemplate(template, { args }); + return tsquery(code, 'AwaitExpression'); + } + } + } catch { + // couldn't covert + } + + const code = tstemplate(` +class Dummy { +public async dummy () { + await <%= interaction %>; +}; +} + `, { interaction: interaction.expression }); + return tsquery(code, 'AwaitExpression'); + }); + + const [lastInteraction] = interactionStatements.reverse(); + + if (returnType && returnType.length) { + const code = tstemplate(` +async function dummy () { + return <%= interaction %>; +} + `, { interaction: lastInteraction }); + const [r] = tsquery(code, 'ReturnStatement'); + interactionStatements[interactionStatements.length - 1] = r; + } + const [type] = returnType || VOID; + + const code = tstemplate(` +class Dummy { + public async <%= name %> (%= parameters %): <%= type %> { + <%= interactionStatements %> + }; +} + `, { name, parameters, interactionStatements, type }); + return tsquery(code, 'MethodDeclaration'); + }); + + const dependencies = tsquery.match(ast, PAGE_OBJECT_DEPENDENCY_QUERY); + + const dependencyImports = dependencies.flatMap(dependency => { + const [name] = tsquery.match(dependency, PAGE_OBJECT_DEPENDENCY_NAME_QUERY); + const [path] = tsquery.match(dependency, PAGE_OBJECT_DEPENDENCY_PATH_QUERY); + path.text = path.text.replace('.po.js', '.page'); + const code = tstemplate(` +import { <%= name %> } from <%= path %> + `, { name, path }); + return tsquery(code, 'ImportDeclaration'); + }); + + + const constructorArguments = []; + if (addSelect || usesHost) { + imports.Selector = true; + let code; + if (usesHost){ + code = tstemplate(` +class Dummy { + constructor (private _select: Selector) { } +} + `); + } else { + imports.createSelector = true; + code = tstemplate(` +class Dummy { + constructor (private _select: Selector = createSelector()) { } +} + `); + } + const [select] = tsquery(code, 'Parameter'); + constructorArguments.push(select); +} + + const migrationHelpers = Object.keys(imports).filter(key => imports[key]).map(key => createIdentifier(key)); + const helperImports = []; + if (migrationHelpers.length) { + const code = tstemplate(`import { <%= migrationHelpers %> } from '@trademe/tractor-to-playwright';`, { migrationHelpers }); + const [migrationHelpersImport] = tsquery(code, 'ImportDeclaration'); + helperImports.push(migrationHelpersImport); + } + + const result = tstemplate(` +import { Page } from '@playwright/test'; + +<%= helperImports %> +<%= dependencyImports %> + +export class <%= className %> { + <%= elementPublicFields %> + + <%= elementPrivateFields %> + + constructor (private _page: Page, %= constructorArguments %) { } + + <%= actionDeclarations %> +} + `, { helperImports, dependencyImports, className, constructorArguments, elementPublicFields: [...elementPublicFields, ...elementGroupFields], elementPrivateFields, actionDeclarations }); + + await newFile.save(result); + + let ts = newFile.content; + ts = ts.replace(`import { Page } from '@playwright/test';`, appendNewLine); + ts = ts.replace(/import { .* } from '@trademe\/tractor-to-playwright';/, appendNewLine); + ts = ts.replace(`export class`, prependNewLine); + ts = ts.replace(` {2}constructor`, prependNewLine); + ts = ts.replace(` {2}constructor`, (input) => prepend(' // tslint:disable-next-line:constructor-params-format\n', input)); + ts = ts.replace(/ {2}public async/g, prependNewLine); + await newFile.save(ts); + return null; +} + +function appendNewLine (input) { + return `${input}\n`; +} + +function prependNewLine (input) { + return prepend(`\n`, input); +} + +function prepend (pre, input) { + return `${pre}${input}`; +} \ No newline at end of file diff --git a/plugins/page-objects/src/upgrade/index.js b/plugins/page-objects/src/upgrade/index.js index e520fc06..2f884852 100644 --- a/plugins/page-objects/src/upgrade/index.js +++ b/plugins/page-objects/src/upgrade/index.js @@ -1,12 +1,13 @@ // Dependencies: import { getConfig } from '@tractor/config-loader'; import { readFiles } from '@tractor/file-structure'; +import { MochaSpecFile } from '../../../mocha-specs/dist/tractor/server/files/mocha-spec-file'; import { PageObjectFile } from '../tractor/server/files/page-object-file'; import { PageObjectFileRefactorer } from '../tractor/server/files/page-object-file-refactorer'; import * as semver from 'semver'; // Versions: -const VERSIONS = ['0.5.0', '0.5.2', '0.6.0', '0.7.0', '1.4.0', '1.7.0']; +const VERSIONS = ['0.5.0', '0.5.2', '0.6.0', '0.7.0', '1.4.0', '1.7.0', '1.9.0']; export async function upgrade () { const config = getConfig(); @@ -15,6 +16,9 @@ export async function upgrade () { let pageObjectsFileStructure; try { pageObjectsFileStructure = await readFiles(config.pageObjects.directory, [PageObjectFile]); + let mochaSpecsFileStructure = await readFiles(config.mochaSpecs.directory, [MochaSpecFile]); + pageObjectsFileStructure.referenceManager.addFileStructure(mochaSpecsFileStructure); + await mochaSpecsFileStructure.read(); } catch { // Can't read .po.js files, giving up. return; @@ -39,7 +43,10 @@ export async function upgrade () { await p; PageObjectFileRefactorer[upgradeVersion] = require(`./${upgradeVersion}`).upgrade; await file.refactor(upgradeVersion); + if (upgradeVersion === '1.9.0') { + return; + } return file.refactor('versionChange', { version: upgradeVersion }); - }, null); - }, null); + }, Promise.resolve()); + }, Promise.resolve()); } diff --git a/plugins/screen-size/package.json b/plugins/screen-size/package.json index 985a1544..03f7df71 100644 --- a/plugins/screen-size/package.json +++ b/plugins/screen-size/package.json @@ -1,6 +1,6 @@ { "name": "@tractor-plugins/screen-size", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "tractor plugin for manipulating screen size", "author": "Craig Spence ", "license": "MIT", @@ -47,11 +47,11 @@ "@tractor/core": "^1.0.0" }, "devDependencies": { - "@tractor/config-loader": "^1.9.4-alpha.4", - "@tractor/dependency-injection": "^1.9.4-alpha.4", - "@tractor/error-handler": "^1.9.4-alpha.4", - "@tractor/logger": "^1.9.4-alpha.4", - "@tractor/plugin-loader": "^1.9.4-alpha.4" + "@tractor/config-loader": "^1.9.4-tractor-to-playwright.0", + "@tractor/dependency-injection": "^1.9.4-tractor-to-playwright.0", + "@tractor/error-handler": "^1.9.4-tractor-to-playwright.0", + "@tractor/logger": "^1.9.4-tractor-to-playwright.0", + "@tractor/plugin-loader": "^1.9.4-tractor-to-playwright.0" }, "gitHead": "aac58387d7addbeceb2683c730fd7801922f7426" } diff --git a/plugins/visual-regression/package.json b/plugins/visual-regression/package.json index b5021e80..00d591a1 100644 --- a/plugins/visual-regression/package.json +++ b/plugins/visual-regression/package.json @@ -1,6 +1,6 @@ { "name": "@tractor-plugins/visual-regression", - "version": "1.9.4-alpha.4", + "version": "1.9.4-tractor-to-playwright.0", "description": "tractor plugin for visual regression testing", "author": "Craig Spence ", "license": "MIT", diff --git a/yarn.lock b/yarn.lock index fab7504f..75a23afa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1900,6 +1900,18 @@ resolved "https://registry.yarnpkg.com/@phenomnomnominal/protractor-use-mocha-hook/-/protractor-use-mocha-hook-0.1.5.tgz#fc91ab6dbd45c7145651a0ff117d033cd9bc66a2" integrity sha512-fo1KuMIBhUReL1fpN/OtuRdZQYKnFCSnMFBwJgfgU4oaF00heWzCBd7VQfDQW/I2vv24s9ekPqKTiywlwhRv3A== +"@phenomnomnominal/tsquery@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@phenomnomnominal/tsquery/-/tsquery-4.1.1.tgz#42971b83590e9d853d024ddb04a18085a36518df" + integrity sha512-jjMmK1tnZbm1Jq5a7fBliM4gQwjxMU7TFoRNwIyzwlO+eHPRCFv/Nv+H/Gi1jc3WR7QURG8D5d0Tn12YGrUqBQ== + dependencies: + esquery "^1.0.1" + +"@phenomnomnominal/tstemplate@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@phenomnomnominal/tstemplate/-/tstemplate-0.1.0.tgz#125b2617fa8cead47433bbf34ba7ef4e30a7c0f2" + integrity sha512-/v+GIVNFHAz4+nQtgy9e5ZAXK3xj6TbP5s9JTpnFuqkcLB+gB2lJ6x/nsDhkKhzR6o4REuzhsYoWYnXqKC/UnQ== + "@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.3.0.tgz#50a2754016b6f30a994ceda6d9a0a8c36adda849" @@ -2151,6 +2163,11 @@ resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.1.tgz#b51cf39a6324a4ede17ef72dfa58a64fbefb39c7" integrity sha512-uCZpakN0HrNsX+rZeXwCPByanpTN+dB3iApOrV4uggG955GZOw1YVziEpH6YDE60/+H95Jztg48UQ0NGZ6TPag== +"@types/prettier@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.2.tgz#fc8c2825e4ed2142473b4a81064e6e081463d1b3" + integrity sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog== + "@types/q@^0.0.32": version "0.0.32" resolved "https://registry.yarnpkg.com/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" @@ -12254,6 +12271,11 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= +prettier@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" + integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== + pretty-format@^24.8.0: version "24.8.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2"