diff --git a/packages/calypso-e2e/src/index.ts b/packages/calypso-e2e/src/index.ts index 22cc0562c8ba07..b3750a222277f2 100644 --- a/packages/calypso-e2e/src/index.ts +++ b/packages/calypso-e2e/src/index.ts @@ -3,6 +3,7 @@ import * as BrowserManager from './browser-manager'; import * as DataHelper from './data-helper'; import * as ElementHelper from './element-helper'; import * as MediaHelper from './media-helper'; +export type { TestFile } from './media-helper'; export { BrowserHelper, BrowserManager, MediaHelper, DataHelper, ElementHelper }; diff --git a/packages/calypso-e2e/src/media-helper.ts b/packages/calypso-e2e/src/media-helper.ts index 52a94d3a75c578..43d6a2d2f0af3f 100644 --- a/packages/calypso-e2e/src/media-helper.ts +++ b/packages/calypso-e2e/src/media-helper.ts @@ -1,9 +1,21 @@ +import { constants } from 'fs'; import fs from 'fs/promises'; import os from 'os'; import path from 'path'; import config from 'config'; import { getTimestamp } from './data-helper'; +/** + * Interface for holding various parts of a filepath. + */ +export interface TestFile { + fullpath: string; // eg. /usr/home/wp-calypso/test/e2e/image-uploads/image.jpg + dirname: string; // eg. /usr/home/wp-calypso/test/e2e/image-uploads/ + basename: string; // eg. image.jpg + filename: string; // eg. image + extension: string; // eg. .jpg +} + const artifacts: { [ key: string ]: string } = config.get( 'artifacts' ); /** @@ -45,61 +57,56 @@ export function getVideoDir(): string { /** * Creates a temporary test file by cloning a source file under a new name. * + * @param {string} sourcePath Full path on disk of the source file. * @param {{[key: string]: string}} param0 Parameter object. - * @param {string} param0.sourceFileName Basename of the source file to be cloned. - * @param {string} [param0.testFileName] Basename of the test file to be generated. - * @returns {Promise} Full path to the generated test file. + * @param {string} [param0.postfix] Additional suffix to be used for the file. + * @returns {Promise} Object implementing the TestFile interface. + * @throws {Error} If source file was not found, or source file did not contain an extension. */ -export async function createTestFile( { - sourceFileName, - testFileName, -}: { - sourceFileName: string; - testFileName?: string; -} ): Promise< string > { - let fileName = getTimestamp(); - // If the output `testFileName` is defined, use that as part of the final filename. - if ( testFileName ) { - fileName += `-${ testFileName }`; +export async function createTestFile( + sourcePath: string, + { + postfix, + }: { + postfix?: string; + } = {} +): Promise< TestFile > { + // Check whether the source file maps to a file. + // Note, if sourcePath is not found use console.error instead of throw: + // https://github.com/facebook/jest/issues/8688 + try { + await fs.access( sourcePath ); + } catch { + throw new Error( `Source file ${ sourcePath } not found on disk.` ); } - // Reassign the variable with the final name to be used, including the extension. - fileName = `${ fileName }.${ sourceFileName.split( '.' ).pop() }`; - - const sourceFileDir = path.join( __dirname, '../../../../../test/e2e/image-uploads/' ); - const sourceFilePath = path.join( sourceFileDir, sourceFileName ); + // Obtain the file extension. + const extension = path.extname( sourcePath ); + if ( ! extension ) { + throw new Error( `Extension not found on source file ${ sourcePath }` ); + } - const tempDir = await fs.mkdtemp( path.join( os.tmpdir(), 'e2e-' ) ); - const testFilePath = path.join( tempDir, fileName ); + // Generate a filename using current timestamp and a pseudo-randomly generated integer. + let filename = getTimestamp(); + // If `postfix` is defined, use that as part of the final filename. + if ( postfix ) { + filename += `-${ postfix }`; + } - await fs.copyFile( sourceFilePath, testFilePath ); + // Obtain the basename (filename with extension) + const basename = `${ filename }${ extension }`; - return testFilePath; -} - -/** - * Returns the path to a generated temporary JPEG image file. - * - * @returns {Promise} Full path on disk to the generated test file. - */ -export async function createTestImage(): Promise< string > { - return await createTestFile( { sourceFileName: 'image0.jpg' } ); -} + const tempDir = await fs.mkdtemp( path.join( os.tmpdir(), 'e2e' ) ); + const targetPath = path.join( tempDir, basename ); -/** - * Returns the path to a generated temporary MP3 audio file. - * - * @returns {string} Full path on disk to the generated test file. - */ -export async function createTestAudio(): Promise< string > { - return await createTestFile( { sourceFileName: 'bees.mp3' } ); -} + await fs.copyFile( sourcePath, targetPath, constants.COPYFILE_EXCL ); -/** - * Returns the path to an unsupported file. - * - * @returns {string} Full path on disk to the generated test file. - */ -export async function createInvalidFile(): Promise< string > { - return await createTestFile( { sourceFileName: 'unsupported_extension.mkv' } ); + // Return an object implementing the interface. + return { + fullpath: targetPath, + dirname: tempDir, + basename: basename, + filename: filename, + extension: extension, + }; } diff --git a/test/e2e/lib/jest/environment.js b/test/e2e/lib/jest/environment.js index 4a5961fccb9021..37cdc6f2c9f255 100644 --- a/test/e2e/lib/jest/environment.js +++ b/test/e2e/lib/jest/environment.js @@ -2,6 +2,7 @@ const JestEnvironmentNode = require( 'jest-environment-node' ); class JestEnvironmentE2E extends JestEnvironmentNode { testFailed = false; + hookFailed = false; async handleTestEvent( event ) { switch ( event.name ) { @@ -16,9 +17,20 @@ class JestEnvironmentE2E extends JestEnvironmentNode { if ( this.testFailed ) { event.test.mode = 'skip'; } + // With this flag enabled, all test cases in the describe + // block will be marked as failed. + // This way all other hooks (screenshot/recording) are run, + // and Jest correctly exits after those hooks are run. + if ( this.hookFailed ) { + event.test.mode = 'fail'; + } break; case 'hook_failure': + this.global.__CURRENT_TEST_FAILED__ = true; + this.hookFailed = true; + break; + case 'test_fn_failure': this.global.__CURRENT_TEST_FAILED__ = true; this.testFailed = true; diff --git a/test/e2e/specs/constants.js b/test/e2e/specs/constants.js new file mode 100644 index 00000000000000..baca2996de5f94 --- /dev/null +++ b/test/e2e/specs/constants.js @@ -0,0 +1,11 @@ +import path from 'path'; + +export const TEST_IMAGE_PATH = path.normalize( + path.join( __dirname, '..', 'image-uploads', 'image0.jpg' ) +); +export const TEST_AUDIO_PATH = path.normalize( + path.join( __dirname, '..', 'image-uploads', 'bees.mp3' ) +); +export const UNSUPPORTED_FILE_PATH = path.normalize( + path.join( __dirname, '..', 'image-uploads', 'unsupported_extension.mkv' ) +); diff --git a/test/e2e/specs/specs-playwright/wp-blocks__coblocks.ts b/test/e2e/specs/specs-playwright/wp-blocks__coblocks.ts index 55bb5a82facde6..a2634a637f950a 100644 --- a/test/e2e/specs/specs-playwright/wp-blocks__coblocks.ts +++ b/test/e2e/specs/specs-playwright/wp-blocks__coblocks.ts @@ -2,7 +2,6 @@ * @group gutenberg */ -import path from 'path'; import { setupHooks, DataHelper, @@ -15,14 +14,16 @@ import { HeroBlock, ClicktoTweetBlock, LogosBlock, + TestFile, } from '@automattic/calypso-e2e'; import { Page } from 'playwright'; +import { TEST_IMAGE_PATH } from '../constants'; -describe( DataHelper.createSuiteTitle( 'Blocks: CoBlocks' ), () => { +describe( DataHelper.createSuiteTitle( 'Blocks: CoBlocks' ), function () { let gutenbergEditorPage: GutenbergEditorPage; let pricingTableBlock: PricingTableBlock; let page: Page; - let logoImage: string; + let logoImage: TestFile; // Test data const pricingTableBlockPrice = 888; @@ -30,12 +31,12 @@ describe( DataHelper.createSuiteTitle( 'Blocks: CoBlocks' ), () => { const clicktoTweetBlockTweet = 'The foolish man seeks happiness in the distance. The wise grows it under his feet. — James Oppenheim'; - setupHooks( ( args ) => { + setupHooks( ( args: { page: Page } ) => { page = args.page; } ); beforeAll( async () => { - logoImage = await MediaHelper.createTestImage(); + logoImage = await MediaHelper.createTestFile( TEST_IMAGE_PATH ); } ); it( 'Log in', async function () { @@ -78,7 +79,7 @@ describe( DataHelper.createSuiteTitle( 'Blocks: CoBlocks' ), () => { it( `Insert ${ LogosBlock.blockName } block and set image`, async function () { const blockHandle = await gutenbergEditorPage.addBlock( LogosBlock.blockName ); const logosBlock = new LogosBlock( blockHandle ); - await logosBlock.upload( logoImage ); + await logosBlock.upload( logoImage.fullpath ); } ); it( 'Publish and visit post', async function () { @@ -104,6 +105,6 @@ describe( DataHelper.createSuiteTitle( 'Blocks: CoBlocks' ), () => { ); it( `Confirm Logos block is visible in published post`, async () => { - await LogosBlock.validatePublishedContent( page, path.parse( logoImage ).name ); + await LogosBlock.validatePublishedContent( page, [ logoImage.filename ] ); } ); } ); diff --git a/test/e2e/specs/specs-playwright/wp-blocks__media-spec.ts b/test/e2e/specs/specs-playwright/wp-blocks__media-spec.ts index f60cee1d9618ed..f87abf0312f036 100644 --- a/test/e2e/specs/specs-playwright/wp-blocks__media-spec.ts +++ b/test/e2e/specs/specs-playwright/wp-blocks__media-spec.ts @@ -3,7 +3,6 @@ * @group gutenberg */ -import path from 'path'; import { setupHooks, DataHelper, @@ -14,13 +13,15 @@ import { ImageBlock, AudioBlock, FileBlock, + TestFile, } from '@automattic/calypso-e2e'; import { Page } from 'playwright'; +import { TEST_IMAGE_PATH, TEST_AUDIO_PATH } from '../constants'; -describe( DataHelper.createSuiteTitle( 'Blocks: Media (Upload)' ), () => { +describe( DataHelper.createSuiteTitle( 'Blocks: Media (Upload)' ), function () { let gutenbergEditorPage: GutenbergEditorPage; let page: Page; - let testFiles: { image: string; image_reserved_name: string; audio: string }; + let testFiles: { image: TestFile; image_reserved_name: TestFile; audio: TestFile }; setupHooks( ( args ) => { page = args.page; @@ -28,12 +29,11 @@ describe( DataHelper.createSuiteTitle( 'Blocks: Media (Upload)' ), () => { beforeAll( async () => { testFiles = { - image: await MediaHelper.createTestImage(), - image_reserved_name: await MediaHelper.createTestFile( { - sourceFileName: 'image0.jpg', - testFileName: 'filewith#?#?reservedurlchars', + image: await MediaHelper.createTestFile( TEST_IMAGE_PATH ), + image_reserved_name: await MediaHelper.createTestFile( TEST_IMAGE_PATH, { + postfix: 'filewith#?#?reservedurlchars', } ), - audio: await MediaHelper.createTestAudio(), + audio: await MediaHelper.createTestFile( TEST_AUDIO_PATH ), }; } ); @@ -55,47 +55,49 @@ describe( DataHelper.createSuiteTitle( 'Blocks: Media (Upload)' ), () => { it( `${ ImageBlock.blockName } block: upload image file`, async function () { const blockHandle = await gutenbergEditorPage.addBlock( ImageBlock.blockName ); const imageBlock = new ImageBlock( blockHandle ); - await imageBlock.upload( testFiles.image ); + await imageBlock.upload( testFiles.image.fullpath ); } ); it( `${ ImageBlock.blockName } block: upload image file with reserved URL characters`, async function () { const blockHandle = await gutenbergEditorPage.addBlock( ImageBlock.blockName ); const imageBlock = new ImageBlock( blockHandle ); - await imageBlock.upload( testFiles.image_reserved_name ); + await imageBlock.upload( testFiles.image_reserved_name.fullpath ); } ); it( `${ AudioBlock.blockName } block: upload audio file`, async function () { const blockHandle = await gutenbergEditorPage.addBlock( AudioBlock.blockName ); const audioBlock = new AudioBlock( blockHandle ); - await audioBlock.upload( testFiles.audio ); + await audioBlock.upload( testFiles.audio.fullpath ); } ); it( `${ FileBlock.blockName } block: upload audio file`, async function () { const blockHandle = await gutenbergEditorPage.addBlock( FileBlock.blockName ); const fileBlock = new FileBlock( blockHandle ); - await fileBlock.upload( testFiles.audio ); + await fileBlock.upload( testFiles.audio.fullpath ); } ); it( 'Publish and visit post', async function () { - await gutenbergEditorPage.publish( { visit: true } ); + // Must save as draft first to bypass issue with post-publish panel being auto-dismissed + // after publishing. May be related to the following issue? + // See https://github.com/Automattic/wp-calypso/issues/54421. + await gutenbergEditorPage.publish( { visit: true, saveDraft: true } ); } ); - it( `Confirm Image block is visible in published post`, async () => { - await ImageBlock.validatePublishedContent( page, path.parse( testFiles.image ).name ); + it( `Confirm Image block is visible in published post`, async function () { + await ImageBlock.validatePublishedContent( page, [ testFiles.image.filename ] ); } ); - it( `Confirm Image block is visible in published post (reserved name)`, async () => { - await ImageBlock.validatePublishedContent( - page, - path.parse( testFiles.image_reserved_name ).name.replace( /[^a-zA-Z ]/g, '' ) - ); + it( `Confirm Image block is visible in published post (reserved name)`, async function () { + await ImageBlock.validatePublishedContent( page, [ + testFiles.image_reserved_name.filename.replace( /[^a-zA-Z ]/g, '' ), + ] ); } ); - it( `Confirm Audio block is visible in published post`, async () => { + it( `Confirm Audio block is visible in published post`, async function () { await AudioBlock.validatePublishedContent( page ); } ); - it( `Confirm File block is visible in published post`, async () => { - await FileBlock.validatePublishedContent( page, path.parse( testFiles.audio ).name ); + it( `Confirm File block is visible in published post`, async function () { + await FileBlock.validatePublishedContent( page, [ testFiles.audio.filename ] ); } ); } ); diff --git a/test/e2e/specs/specs-playwright/wp-media__edit-spec.js b/test/e2e/specs/specs-playwright/wp-media__edit-spec.ts similarity index 86% rename from test/e2e/specs/specs-playwright/wp-media__edit-spec.js rename to test/e2e/specs/specs-playwright/wp-media__edit-spec.ts index cbbfff2c0ba29f..9247b75630db29 100644 --- a/test/e2e/specs/specs-playwright/wp-media__edit-spec.js +++ b/test/e2e/specs/specs-playwright/wp-media__edit-spec.ts @@ -9,18 +9,21 @@ import { MediaPage, SidebarComponent, setupHooks, + TestFile, } from '@automattic/calypso-e2e'; +import { Page } from 'playwright'; +import { TEST_IMAGE_PATH } from '../constants'; describe( DataHelper.createSuiteTitle( 'Media: Edit Media' ), function () { - let testImage; - let page; + let testImage: TestFile; + let page: Page; setupHooks( ( args ) => { page = args.page; } ); beforeAll( async () => { - testImage = await MediaHelper.createTestImage(); + testImage = await MediaHelper.createTestFile( TEST_IMAGE_PATH ); } ); describe.each` @@ -28,7 +31,7 @@ describe( DataHelper.createSuiteTitle( 'Media: Edit Media' ), function () { ${ 'Simple' } | ${ 'defaultUser' } ${ 'Atomic' } | ${ 'wooCommerceUser' } `( 'Edit Image ($siteType)', function ( { user } ) { - let mediaPage; + let mediaPage: MediaPage; it( 'Log In', async function () { const loginFlow = new LoginFlow( page, user ); @@ -51,7 +54,7 @@ describe( DataHelper.createSuiteTitle( 'Media: Edit Media' ), function () { it( 'Upload image', async function () { // Ideally, we'd not want to upload an image (that's a separate test) // but occasionally, the photo gallery is cleaned out leaving no images. - const uploadedImageHandle = await mediaPage.upload( testImage ); + const uploadedImageHandle = await mediaPage.upload( testImage.fullpath ); const isVisible = await uploadedImageHandle.isVisible(); expect( isVisible ).toBe( true ); } ); diff --git a/test/e2e/specs/specs-playwright/wp-media__upload-spec.js b/test/e2e/specs/specs-playwright/wp-media__upload-spec.ts similarity index 67% rename from test/e2e/specs/specs-playwright/wp-media__upload-spec.js rename to test/e2e/specs/specs-playwright/wp-media__upload-spec.ts index 90b4a40adfea13..d378a0a535c256 100644 --- a/test/e2e/specs/specs-playwright/wp-media__upload-spec.js +++ b/test/e2e/specs/specs-playwright/wp-media__upload-spec.ts @@ -10,11 +10,14 @@ import { MediaPage, SidebarComponent, MediaHelper, + TestFile, } from '@automattic/calypso-e2e'; +import { Page } from 'playwright'; +import { TEST_IMAGE_PATH, TEST_AUDIO_PATH, UNSUPPORTED_FILE_PATH } from '../constants'; describe( DataHelper.createSuiteTitle( 'Media: Upload' ), () => { - let testFiles; - let page; + let testFiles: { image: TestFile; audio: TestFile; unsupported: TestFile }; + let page: Page; setupHooks( ( args ) => { page = args.page; @@ -22,9 +25,9 @@ describe( DataHelper.createSuiteTitle( 'Media: Upload' ), () => { beforeAll( async () => { testFiles = { - image: await MediaHelper.createTestImage(), - audio: await MediaHelper.createTestAudio(), - invalid: await MediaHelper.createInvalidFile(), + image: await MediaHelper.createTestFile( TEST_IMAGE_PATH ), + audio: await MediaHelper.createTestFile( TEST_AUDIO_PATH ), + unsupported: await MediaHelper.createTestFile( UNSUPPORTED_FILE_PATH ), }; } ); @@ -34,7 +37,7 @@ describe( DataHelper.createSuiteTitle( 'Media: Upload' ), () => { ${ 'Simple' } | ${ 'defaultUser' } ${ 'Atomic' } | ${ 'wooCommerceUser' } `( 'Upload media files ($siteType)', ( { user } ) => { - let mediaPage; + let mediaPage: MediaPage; it( 'Log In', async function () { const loginFlow = new LoginFlow( page, user ); @@ -51,18 +54,18 @@ describe( DataHelper.createSuiteTitle( 'Media: Upload' ), () => { } ); it( 'Upload image and confirm addition to gallery', async () => { - const uploadedItem = await mediaPage.upload( testFiles.image ); + const uploadedItem = await mediaPage.upload( testFiles.image.fullpath ); assert.strictEqual( await uploadedItem.isVisible(), true ); } ); it( 'Upload audio and confirm addition to gallery', async () => { - const uploadedItem = await mediaPage.upload( testFiles.audio ); + const uploadedItem = await mediaPage.upload( testFiles.audio.fullpath ); assert.strictEqual( await uploadedItem.isVisible(), true ); } ); it( 'Upload an unsupported file type and see the rejection notice', async function () { try { - await mediaPage.upload( testFiles.invalid ); + await mediaPage.upload( testFiles.unsupported.fullpath ); } catch ( error ) { assert.match( error.message, /could not be uploaded/i ); } diff --git a/test/e2e/tsconfig.json b/test/e2e/tsconfig.json index e8e24bbf696b68..0ff0821b4906eb 100644 --- a/test/e2e/tsconfig.json +++ b/test/e2e/tsconfig.json @@ -1,9 +1,9 @@ { "extends": "@automattic/calypso-build/typescript/ts-package.json", "compilerOptions": { + "allowJs": true, "noEmit": true, // just type checking, no output. The output is handled by babel. "types": [ "jest", "node" ] // no mocha - we are only using TypeScript for the new Playwright scripts }, - // TypeScript is scoped only for the new Playwright scripts - "include": [ "specs/specs-playwright", "lib/gutenberg/tracking" ] + "include": [ "specs/specs-playwright", "specs/constants.js", "lib/gutenberg/tracking" ] }