Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: convert screenshots.js to screenshots.ts #30758

Merged
merged 16 commits into from
Dec 20, 2024
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
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'
import { stat } from 'fs/promises'

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

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

interface Data {
specName: string
name: string
titles?: string[]
testFailure?: boolean
testAttemptIndex?: number
overwrite?: boolean
simple?: boolean
current?: number
total?: number
}

// 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 +48,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 @@ -115,7 +143,7 @@ const captureAndCheck = function (data, automate, conditionFn) {

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

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

Expand Down Expand Up @@ -187,7 +215,7 @@ const pixelConditionFn = function (data, image) {
return passes
}

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

const clearMultipartState = function () {
debug('clearing %d cached multipart images', multipartImages.length)
Expand Down Expand Up @@ -246,7 +274,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 @@ -276,9 +304,15 @@ const getBuffer = function (details) {
return Promise.resolve(details.buffer)
}

return Promise
.promisify(details.image.getBuffer)
.call(details.image, Jimp.AUTO)
return new Promise((resolve, reject) => {
details.image.getBuffer(Jimp.AUTO, (err, val) => {
if (err) {
reject(err)
} else {
resolve(val)
}
})
})
}

const getDimensions = function (details) {
Expand Down Expand Up @@ -316,7 +350,7 @@ const ensureSafePath = function (withoutExt, extension, overwrite, num = 0) {
}

// path does not exist, attempt to create it to check for an ENAMETOOLONG error
return fs.outputFileAsync(fullPath, '')
return fs.outputFile(fullPath, '')
.then(() => fullPath)
.catch((err) => {
debug('received error when testing path %o', { err, fullPath, maxSafePrefixBytes, maxSafeBytes })
Expand All @@ -332,24 +366,25 @@ 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: string, overwrite: Data['overwrite']) {
let names
const specNames = (data.specName || '')
.split(pathSeparatorRe)

if (data.name) {
names = data.name.split(pathSeparatorRe).map(sanitize)
names = data.name.split(pathSeparatorRe).map((name: string) => sanitize(name))
} else {
names = _
.chain(data.titles)
.map(sanitizeToString)
jennifer-shehane marked this conversation as resolved.
Show resolved Hide resolved
.map((title: string | null | undefined) => sanitizeToString(title))
.join(RUNNABLE_SEPARATOR)
// @ts-expect-error - this shouldn't be necessary, but it breaks if you remove it
.concat([])
.value()
}
Expand All @@ -370,13 +405,13 @@ const getPath = function (data, ext, screenshotsFolder, overwrite) {
return ensureSafePath(withoutExt, ext, overwrite)
}

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

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

module.exports = {
export = {
crop,

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

imagesMatch,

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

debug('capturing screenshot %o', data)
Expand Down Expand Up @@ -461,17 +496,17 @@ module.exports = {
})
},

save (data, details, screenshotsFolder) {
save (data: Data, details, screenshotsFolder: string) {
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(() => {
return fs.statAsync(pathToScreenshot).get('size')
}).then((size) => {
return stat(pathToScreenshot)
}).then(({ size }) => {
const dimensions = getDimensions(details)

const { multipart, pixelRatio, takenAt } = details
Expand All @@ -492,7 +527,7 @@ module.exports = {
},

afterScreenshot (data, details) {
const duration = new Date() - new Date(data.startTime)
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
Loading