Skip to content

Commit

Permalink
chore: convert screenshots.js to screenshots.ts (#30758)
Browse files Browse the repository at this point in the history
* chore: convert screenshots.js to screenshots.ts

* Fix export

* Try to clean up types

* Add some more types around data

* Add some more info around types to the screenshots file

* try to fix the error in screenshots

* fix type of overwrite

* revert everything in that getPath function (just dont touch it)

* more reverting

* fix the thenable

* revert again

* revert outputFile change
  • Loading branch information
jennifer-shehane authored Dec 20, 2024
1 parent 07d9c1b commit 0a8a130
Showing 1 changed file with 118 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,74 @@
const _ = require('lodash')
const mime = require('mime')
const path = require('path')
const Promise = require('bluebird')
const dataUriToBuffer = require('data-uri-to-buffer')
const Jimp = require('jimp')
const sizeOf = require('image-size')
const colorString = require('color-string')
const sanitize = require('sanitize-filename')
let debug = require('debug')('cypress:server:screenshot')
const plugins = require('./plugins')
const { fs } = require('./util/fs')

import _ from 'lodash'
import Debug from 'debug'
import mime from 'mime'
import path from 'path'
import Promise from 'bluebird'
import dataUriToBuffer from 'data-uri-to-buffer'
import Jimp from 'jimp'
import sizeOf from 'image-size'
import colorString from 'color-string'
import sanitize from 'sanitize-filename'
import * as plugins from './plugins'
import { fs } from './util/fs'

let debug = Debug('cypress:server:screenshot')
const RUNNABLE_SEPARATOR = ' -- '
const pathSeparatorRe = /[\\\/]/g

// internal id incrementor
let __ID__ = null
let __ID__: string | null = null

type ScreenshotsFolder = string | false | undefined

interface Clip {
x: number
y: number
width: number
height: number
}

// TODO: This is likely not representative of the entire Type and should be updated
interface Data {
specName: string
name: string
startTime: Date
viewport: {
width: number
height: number
}
titles?: string[]
testFailure?: boolean
overwrite?: boolean
simple?: boolean
current?: number
total?: number
testAttemptIndex?: number
appOnly?: boolean
hideRunnerUi?: boolean
clip?: Clip
userClip?: Clip
}

// TODO: This is likely not representative of the entire Type and should be updated
interface Details {
image: any
pixelRatio: any
multipart: any
takenAt: Date
}

// TODO: This is likely not representative of the entire Type and should be updated
interface SavedDetails {
size?: string
takenAt?: Date
dimensions?: string
multipart?: any
pixelRatio?: number
name?: any
specName?: string
testFailure?: boolean
path?: string
}

// many filesystems limit filename length to 255 bytes/characters, so truncate the filename to
// the smallest common denominator of safe filenames, which is 255 bytes. when ENAMETOOLONG
Expand All @@ -34,18 +87,32 @@ const MIN_PREFIX_BYTES = 64
// screenshot id to the debug logs for easier association
debug = _.wrap(debug, (fn, str, ...args) => {
return fn(`(${__ID__}) ${str}`, ...args)
})
}) as Debug.Debugger

const isBlack = (rgba) => {
interface RGBA {
r: number
g: number
b: number
a: number
}

const isBlack = (rgba: RGBA): boolean => {
return `${rgba.r}${rgba.g}${rgba.b}` === '000'
}

const isWhite = (rgba) => {
const isWhite = (rgba: RGBA): boolean => {
return `${rgba.r}${rgba.g}${rgba.b}` === '255255255'
}

const intToRGBA = function (int) {
const obj = Jimp.intToRGBA(int)
interface RGBAWithName extends RGBA {
name?: string
isNotWhite?: boolean
isWhite?: boolean
isBlack?: boolean
}

const intToRGBA = function (int: number): RGBAWithName {
const obj: RGBAWithName = Jimp.intToRGBA(int) as RGBAWithName

if (debug.enabled) {
obj.name = colorString.to.keyword([
Expand Down Expand Up @@ -108,14 +175,14 @@ const hasHelperPixels = function (image, pixelRatio) {
)
}

const captureAndCheck = function (data, automate, conditionFn) {
const captureAndCheck = function (data: Data, automate, conditionFn) {
let attempt
const start = new Date()
let tries = 0

return (attempt = function () {
tries++
const totalDuration = new Date() - start
const totalDuration = new Date().getTime() - start.getTime()

debug('capture and check %o', { tries, totalDuration })

Expand All @@ -140,7 +207,7 @@ const captureAndCheck = function (data, automate, conditionFn) {
})()
}

const isMultipart = (data) => {
const isMultipart = (data: Data) => {
return _.isNumber(data.current) && _.isNumber(data.total)
}

Expand All @@ -166,7 +233,7 @@ const crop = function (image, dimensions, pixelRatio = 1) {
return image.clone().crop(x, y, width, height)
}

const pixelConditionFn = function (data, image) {
const pixelConditionFn = function (data: Data, image) {
const pixelRatio = image.bitmap.width / data.viewport.width

const hasPixels = hasHelperPixels(image, pixelRatio)
Expand All @@ -187,7 +254,7 @@ const pixelConditionFn = function (data, image) {
return passes
}

let multipartImages = []
let multipartImages: { data: Data, image, takenAt, __ID__ }[] = []

const clearMultipartState = function () {
debug('clearing %d cached multipart images', multipartImages.length)
Expand All @@ -199,7 +266,7 @@ const imagesMatch = (img1, img2) => {
return img1.bitmap.data.equals(img2.bitmap.data)
}

const lastImagesAreDifferent = function (data, image) {
const lastImagesAreDifferent = function (data: Data, image) {
// ensure the previous image isn't the same,
// which might indicate the page has not scrolled yet
const previous = _.last(multipartImages)
Expand All @@ -222,7 +289,7 @@ const lastImagesAreDifferent = function (data, image) {
return !matches
}

const multipartConditionFn = function (data, image) {
const multipartConditionFn = function (data: Data, image) {
if (data.current === 1) {
return pixelConditionFn(data, image) && lastImagesAreDifferent(data, image)
}
Expand All @@ -246,7 +313,7 @@ const stitchScreenshots = function (pixelRatio) {

debug(`stitch ${multipartImages.length} images together`)

const takenAts = []
const takenAts: string[] = []
let heightMarker = 0
const fullImage = new Jimp(fullWidth, fullHeight)

Expand Down Expand Up @@ -278,6 +345,7 @@ const getBuffer = function (details) {

return Promise
.promisify(details.image.getBuffer)
// @ts-expect-error
.call(details.image, Jimp.AUTO)
}

Expand All @@ -293,7 +361,7 @@ const getDimensions = function (details) {
return pick(details.image.bitmap)
}

const ensureSafePath = function (withoutExt, extension, overwrite, num = 0) {
const ensureSafePath = function (withoutExt: string, extension: string, overwrite: Data['overwrite'], num = 0) {
const suffix = `${(num && !overwrite) ? ` (${num})` : ''}.${extension}`

const maxSafePrefixBytes = maxSafeBytes - suffix.length
Expand All @@ -316,6 +384,7 @@ const ensureSafePath = function (withoutExt, extension, overwrite, num = 0) {
}

// path does not exist, attempt to create it to check for an ENAMETOOLONG error
// @ts-expect-error
return fs.outputFileAsync(fullPath, '')
.then(() => fullPath)
.catch((err) => {
Expand All @@ -332,24 +401,26 @@ const ensureSafePath = function (withoutExt, extension, overwrite, num = 0) {
})
}

const sanitizeToString = (title) => {
const sanitizeToString = (title: string | null | undefined) => {
// test titles may be values which aren't strings like
// null or undefined - so convert before trying to sanitize
return sanitize(_.toString(title))
}

const getPath = function (data, ext, screenshotsFolder, overwrite) {
const getPath = function (data: Data, ext, screenshotsFolder: ScreenshotsFolder, overwrite: Data['overwrite']) {
let names
const specNames = (data.specName || '')
.split(pathSeparatorRe)

if (data.name) {
// @ts-expect-error
names = data.name.split(pathSeparatorRe).map(sanitize)
} else {
names = _
.chain(data.titles)
.map(sanitizeToString)
.join(RUNNABLE_SEPARATOR)
// @ts-expect-error - this shouldn't be necessary, but it breaks if you remove it
.concat([])
.value()
}
Expand All @@ -361,22 +432,28 @@ const getPath = function (data, ext, screenshotsFolder, overwrite) {
names[index] = `${names[index]} (failed)`
}

if (data.testAttemptIndex > 0) {
if (data.testAttemptIndex && data.testAttemptIndex > 0) {
names[index] = `${names[index]} (attempt ${data.testAttemptIndex + 1})`
}

const withoutExt = path.join(screenshotsFolder, ...specNames, ...names)
let withoutExt

if (screenshotsFolder) {
withoutExt = path.join(screenshotsFolder, ...specNames, ...names)
} else {
withoutExt = path.join(...specNames, ...names)
}

return ensureSafePath(withoutExt, ext, overwrite)
}

const getPathToScreenshot = function (data, details, screenshotsFolder) {
const getPathToScreenshot = function (data: Data, details: Details, screenshotsFolder: ScreenshotsFolder) {
const ext = mime.getExtension(getType(details))

return getPath(data, ext, screenshotsFolder, data.overwrite)
}

module.exports = {
export = {
crop,

getPath,
Expand All @@ -385,7 +462,7 @@ module.exports = {

imagesMatch,

capture (data, automate) {
capture (data: Data, automate) {
__ID__ = _.uniqueId('s')

debug('capturing screenshot %o', data)
Expand Down Expand Up @@ -419,7 +496,7 @@ module.exports = {
debug(`multi-part ${data.current}/${data.total}`)
}

if (multipart && (data.total > 1)) {
if (multipart && (data.total && data.total > 1)) {
// keep previous screenshot partials around b/c if two screenshots are
// taken in a row, the UI might not be caught up so we need something
// to compare the new one to
Expand Down Expand Up @@ -461,15 +538,16 @@ module.exports = {
})
},

save (data, details, screenshotsFolder) {
save (data: Data, details: Details, screenshotsFolder: ScreenshotsFolder) {
return getPathToScreenshot(data, details, screenshotsFolder)
.then((pathToScreenshot) => {
debug('save', pathToScreenshot)

return getBuffer(details)
.then((buffer) => {
return fs.outputFileAsync(pathToScreenshot, buffer)
return fs.outputFile(pathToScreenshot, buffer)
}).then(() => {
// @ts-expect-error TODO: size is not assignable here
return fs.statAsync(pathToScreenshot).get('size')
}).then((size) => {
const dimensions = getDimensions(details)
Expand All @@ -491,8 +569,8 @@ module.exports = {
})
},

afterScreenshot (data, details) {
const duration = new Date() - new Date(data.startTime)
afterScreenshot (data: Data, details: SavedDetails) {
const duration = new Date().getTime() - new Date(data.startTime).getTime()

details = _.extend({}, data, details, { duration })
details = _.pick(details, 'testAttemptIndex', 'size', 'takenAt', 'dimensions', 'multipart', 'pixelRatio', 'name', 'specName', 'testFailure', 'path', 'scaled', 'blackout', 'duration')
Expand Down

4 comments on commit 0a8a130

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0a8a130 Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.17.1/linux-x64/develop-0a8a1305fb0a6cc7846652613285702e580cd58a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0a8a130 Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.17.1/linux-arm64/develop-0a8a1305fb0a6cc7846652613285702e580cd58a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0a8a130 Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.17.1/darwin-arm64/develop-0a8a1305fb0a6cc7846652613285702e580cd58a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0a8a130 Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.17.1/darwin-x64/develop-0a8a1305fb0a6cc7846652613285702e580cd58a/cypress.tgz

Please sign in to comment.