Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
- name: Use Node.js 22.22.0
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22.22.0
cache: 'npm'

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

strategy:
matrix:
node-version: [18.x, 20.x, 22.6] # https://github.com/nodejs/node/issues/54532
node-version: [20.x, 22.x, 24.x]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Build and publish with a supported Node version

The fresh evidence in the current tree is that this matrix was updated but still retains 20.x, while the same change declares Node >=22.22.0 and locks react-router@8.3.0, which has that same minimum; moreover, the Build and Publish steps at lines 31–36 run exclusively for this unsupported Node 20 job. The unchanged e2e workflow also installs and builds on Node 20, so CI continues exercising the upgraded toolchain outside its supported runtime; move these build/publish conditions and the e2e job to at least Node 22.22.

Useful? React with 👍 / 👎.


steps:
- uses: actions/checkout@v4
Expand Down
10 changes: 5 additions & 5 deletions .mocharc-jsdom.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ module.exports = {
extension: ["js", "jsx", "ts", "tsx"],
recursive: true,
require: [
"./test/setup/ts-node",
"./test/setup/jsdom",
"./test/setup/globals",
"./test/setup/assert-ext",
"./test/setup/configure-testing-library"
"./test/setup/ts-node.js",
"./test/setup/jsdom.js",
"./test/setup/globals.js",
"./test/setup/assert-ext.js",
"./test/setup/configure-testing-library.js"
],
};
8 changes: 4 additions & 4 deletions .mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ module.exports = {
extension: ["js", "jsx", "ts", "tsx"],
recursive: true,
require: [
"./test/setup/ts-node",
"./test/setup/globals",
"./test/setup/assert-ext",
"./test/setup/configure-testing-library"
"./test/setup/ts-node.js",
"./test/setup/globals.js",
"./test/setup/assert-ext.js",
"./test/setup/configure-testing-library.js"
],
};
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
legacy-peer-deps=true

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

переехали на react19, но semantic-ui-react его еще не поддерживает. В рамках PR решил не заниматься выпиливанием этого пакета, хотя пора бы его удалить

5 changes: 5 additions & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = {
extends: ['@commitlint/config-conventional']
};
8 changes: 4 additions & 4 deletions lib/adapters/test-result/playwright.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';
import {TestCase as PlaywrightTestCase, TestResult as PlaywrightTestResult} from '@playwright/test/reporter';
import sizeOf from 'image-size';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

image-size уже deprecated, уязвимости не пофиксятся

import {getImageSize} from '../../image-size';
import _ from 'lodash';
import stripAnsi from 'strip-ansi';

Expand All @@ -16,7 +16,6 @@ import {
ImageFile,
ImageInfoDiff,
ImageInfoFull, ImageInfoNoRef, ImageInfoPageError, ImageInfoPageSuccess, ImageInfoSuccess, ImageInfoUpdated,
ImageSize,
TestError, TestStepCompressed, TestStepKey
} from '../../types';
import type {CoordBounds} from 'looks-same';
Expand Down Expand Up @@ -58,6 +57,7 @@ export interface TestResultWithGuiStatus extends Omit<PlaywrightTestResult, 'sta
}

const ANY_IMAGE_ENDING_REGEXP = new RegExp(Object.values(ImageTitleEnding).map(ending => `${ending}$`).join('|'));
const SCREENSHOT_COMPARISON_ERROR_REGEXP = /Screenshot comparison failed|expect\(.*\)\.toHaveScreenshot\(expected\) failed/;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

матчимся на оба текста скриншотного падения в разных версиях pwt, чтобы правильно покрасить цвет скриншотного ретрая


export const DEFAULT_DIFF_OPTIONS = {
diffColor: '#ff00ff'
Expand Down Expand Up @@ -150,7 +150,7 @@ const getImageData = (attachment: PlaywrightAttachment | undefined): PlaywrightI

return {
path: attachment.path as string,
size: !attachment.size ? _.pick(sizeOf(attachment.path as string), ['height', 'width']) as ImageSize : attachment.size,
size: attachment.size || getImageSize(attachment.path as string),
relativePath: attachment.relativePath || path.relative(process.cwd(), attachment.path as string)
};
};
Expand Down Expand Up @@ -212,7 +212,7 @@ export class PlaywrightTestResultAdapter implements ReporterTestResult {

if (/snapshot .*doesn't exist/.test(message) && message.includes('.png')) {
result.name = ErrorName.NO_REF_IMAGE;
} else if (message.includes('Screenshot comparison failed')) {
} else if (SCREENSHOT_COMPARISON_ERROR_REGEXP.test(message)) {
result.name = ErrorName.IMAGE_DIFF;
}

Expand Down
4 changes: 3 additions & 1 deletion lib/adapters/tool/testplane/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,11 @@ export class TestplaneToolAdapter implements ToolAdapter {
const replMode = getReplModeOption(cliTool);
const runner = createTestRunner(testCollectionAdapter.original, tests);
const inspectMode = (inspect || inspectBrk) && {inspect, inspectBrk};
// Testplane 8 uses devtools; Testplane 9 ignores this option.
const runOptions = {grep, sets, tag, browsers, inspectMode, replMode, devtools, local, requireModules};

return runner.run((collection) =>
this._tool.run(collection, {grep, sets, tag, browsers, inspectMode, devtools, replMode, local, requireModules})
this._tool.run(collection, runOptions)
);
}

Expand Down
2 changes: 1 addition & 1 deletion lib/cli/commands/gui.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = (cliTool, toolAdapter) => {
.command(`${commandName} [paths...]`)
.allowUnknownOption()
.description('update the changed screenshots or gather them if they does not exist')
.option('-p, --port <port>', 'Port to launch server on', 8000)
.option('-p, --port <port>', 'Port to launch server on', Number, 8000)
.option('--hostname <hostname>', 'Hostname to launch server on', 'localhost')
.option('-a, --auto-run', 'auto run immediately')
.option('-O, --no-open', 'not to open a browser window after starting the server')
Expand Down
19 changes: 14 additions & 5 deletions lib/common-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import crypto from 'crypto';
import SparkMD5 from 'spark-md5';
import {isEmpty, pick} from 'lodash';
import url from 'url';
import type {AxiosRequestConfig} from 'axios';
import {
ERROR,
Expand Down Expand Up @@ -30,7 +29,13 @@ import type {
import type {ReporterTestResult} from './adapters/test-result';

export const getShortMD5 = (str: string): string => {
return crypto.createHash('md5').update(str, 'ascii').digest('hex').substr(0, 7);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

удаляем, чтобы убрать crypto-browserify из браузерной сборки

let asciiString = '';

for (let index = 0; index < str.length; index++) {
asciiString += String.fromCharCode(str.charCodeAt(index) % 256);
}

return SparkMD5.hashBinary(asciiString).substring(0, 7);
};

const statusPriority: TestStatus[] = [
Expand Down Expand Up @@ -191,9 +196,13 @@ export const isUrl = (str: string): boolean => {
return false;
}

const parsedUrl = url.parse(str);
try {
const parsedUrl = new URL(str);

return !!parsedUrl.host && !!parsedUrl.protocol;
return Boolean(parsedUrl.host && parsedUrl.protocol);
} catch {
return false;
}
};

export const fetchFile = async <T = unknown>(url: string, options?: AxiosRequestConfig) : Promise<{data: T | null, status: number | string, error?: unknown}> => {
Expand Down
3 changes: 2 additions & 1 deletion lib/db-utils/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

import {isEmpty} from 'lodash';
/** @type Record<string, (...args: unknown[]) => unknown> */
import commonSqliteUtils, {handleDatabases, mergeTables, compareDatabaseRowsByTimestamp, selectAllQuery} from './common';
import * as commonSqliteUtils from './common';
import {handleDatabases, mergeTables, compareDatabaseRowsByTimestamp, selectAllQuery} from './common';
import {fetchFile, normalizeUrls} from '../common-utils';

import {DB_SUITES_TABLE_NAME, LOCAL_DATABASE_NAME} from '../constants/database';
Expand Down
7 changes: 6 additions & 1 deletion lib/gui/listen-with-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ const listenOnPort = (server: Express, portToTry: number, hostname?: string): Pr
reject(error);
};

const onListen = (): void => {
const onListen = (error?: Error): void => {
if (error) {
reject(error);
return;
}

resolve();
};

Expand Down
1 change: 1 addition & 0 deletions lib/gui/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const start = async (args: ServerArgs): Promise<ServerReadyData> => {
const app = App.create(args);
const server = express();

server.set('query parser', 'extended');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

для обратной совместимости при переходе на express@5

server.use(bodyParser.json({limit: MAX_REQUEST_SIZE}));

await guiApi.initServer(server);
Expand Down
28 changes: 28 additions & 0 deletions lib/image-size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import fs from 'fs';
import {sync as probeImageSize} from 'probe-image-size';
import type {ImageSize} from './types';

const MAX_HEADER_SIZE = 512 * 1024;

export const getImageSize = (image: string | Buffer): ImageSize => {
let buffer: Buffer;

if (typeof image === 'string') {
const descriptor = fs.openSync(image, 'r');
try {
buffer = Buffer.alloc(Math.min(fs.fstatSync(descriptor).size, MAX_HEADER_SIZE));
fs.readSync(descriptor, buffer, 0, buffer.length, 0);
Comment on lines +13 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Read far enough to find valid image dimensions

For valid JPEGs containing multiple large ICC/EXIF APP segments, the SOF dimension marker can occur after the first 512 KiB. This truncates such files before passing them to probeImageSize, causing getImageSize to throw and abort report generation, whereas the previous image-size path read the complete file. Read progressively until dimensions are found, or fall back to the complete file when the capped prefix is insufficient.

Useful? React with 👍 / 👎.

} finally {
fs.closeSync(descriptor);
}
} else {
buffer = image;
}

const size = probeImageSize(buffer);
if (!size) {
throw new TypeError('Unsupported or invalid image');
}

return {width: size.width, height: size.height};
};
8 changes: 4 additions & 4 deletions lib/images-info-saver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import util from 'util';
import makeDebug from 'debug';
import EventEmitter2 from 'eventemitter2';
import fs from 'fs-extra';
import sizeOf from 'image-size';
import {getImageSize} from './image-size';
import _ from 'lodash';
import PQueue from 'p-queue';

Expand All @@ -14,7 +14,7 @@ import {
ImageFileSaver,
ImageInfoDiff,
ImageInfoFull,
ImageSize, TestSpecByPath
TestSpecByPath
} from './types';
import {copyAndUpdate, removeBufferFromImagesInfo} from './adapters/test-result/utils';
import {cacheDiffImages} from './image-cache';
Expand Down Expand Up @@ -120,7 +120,7 @@ export class ImagesInfoSaver extends EventEmitter2 {
cacheDiffImages.set(hash, filePath);
}

return {path: filePath, size: _.pick(sizeOf(filePath), ['height', 'width']) as ImageSize};
return {path: filePath, size: getImageSize(filePath)};
}

private _getReusedExpectedPath(testResult: TestSpecByPath, imagesInfo: ImageInfoFull): string | null {
Expand Down Expand Up @@ -205,7 +205,7 @@ export class ImagesInfoSaver extends EventEmitter2 {
const newDiffPath = await this._saveImage(diffImg, reportDiffPath);
logger(`Saved diff image from ${(diffImg as ImageFile).path ?? '<buffer>'} to ${newDiffPath}`);

const size = _.pick(sizeOf(isImageBufferData(diffImg) ? Buffer.from(diffImg.buffer) : diffImg.path), ['height', 'width']) as ImageSize;
const size = getImageSize(isImageBufferData(diffImg) ? Buffer.from(diffImg.buffer) : diffImg.path);

return {path: newDiffPath, size};
}
Expand Down
2 changes: 1 addition & 1 deletion lib/static/components/controls/control-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default class ControlButton extends Component<ControlButtonProps> {
dataTestId: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};

render(): JSX.Element {
render(): React.JSX.Element {
const {
label,
title,
Expand Down
4 changes: 3 additions & 1 deletion lib/static/components/controls/report-info.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {Button, Icon, Label} from '@gravity-ui/uikit';
import {isEmpty} from 'lodash';
import {version} from '../../../../package.json';
import packageJson from '../../../../package.json';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

вебпак кидает ворнинги в старом формате:

Should not import the named export 'version' ...
(only default export is available soon)

import useLocalStorage from '@/static/hooks/useLocalStorage';
import {LocalStorageKey, UiMode} from '@/constants/local-storage';
import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics';

const {version} = packageJson;

function ReportInfo(props) {
const analytics = useAnalytics();
const {gui, timestamp} = props;
Expand Down
4 changes: 2 additions & 2 deletions lib/static/components/section/body/page-screenshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ interface PageScreenshotProps {
}

export class PageScreenshot extends Component<PageScreenshotProps> {
render(): JSX.Element {
render(): React.JSX.Element {
return <Details
title="Page screenshot"
content={(): JSX.Element => <Screenshot image={this.props.image} />}
content={(): React.JSX.Element => <Screenshot image={this.props.image} />}
/>;
}
}
2 changes: 1 addition & 1 deletion lib/static/components/test-repeater/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {useDispatch, useSelector} from '@/static/new-ui/modules/react-redux';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

просто типизированная обертка, чтобы не описывать типы каждый раз

import {NumberInput, Button, Icon} from '@gravity-ui/uikit';
import {Plus, Minus} from '@gravity-ui/icons';
import styles from './index.module.css';
Expand Down
5 changes: 2 additions & 3 deletions lib/static/modules/actions/static-accepter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import {getBlobWithRetires} from '../utils';
import {storeCommitInLocalStorage} from '../static-image-accepter';
import actionNames from '../action-names';
import defaultState from '../default-state';
import type {Action, Dispatch, Store} from './types';
import {ThunkAction} from 'redux-thunk';
import type {Action, Dispatch} from './types';
import {Point} from '@/static/new-ui/types';
import {closeModal, openModal} from '@/static/modules/actions/modals';
import {createNotification, createNotificationError} from '@/static/modules/actions/notifications';
Expand Down Expand Up @@ -106,7 +105,7 @@ export const staticAccepterCommitScreenshot = (
axiosRequestOptions = {},
meta
}: StaticAccepterCommitScreenshotOptions
): ThunkAction<Promise<CommitResult>, Store, void, StaticAccepterCommitScreenshotAction> => {
) => {
return (dispatch: Dispatch): Promise<CommitResult> => {
// #region static accepter v2 path
if (moduleUrl) {
Expand Down
4 changes: 0 additions & 4 deletions lib/static/modules/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,10 @@ import {SettingsAction} from '@/static/modules/actions/settings';
import {ProcessingAction} from '@/static/modules/actions/processing';
import {StaticAccepterAction} from '@/static/modules/actions/static-accepter';
import type actionNames from '../action-names';
import type defaultState from '../default-state';
import type {Tree} from '../../../tests-tree-builder/base';
import {FeaturesAction} from '@/static/modules/actions/features';
import {SnapshotsAction} from '@/static/modules/actions/snapshots';
import {BrowsersAction} from '@/static/modules/actions/browsers';

export type Store = Omit<typeof defaultState, 'tree'> & {tree: Tree};

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

нигде больше не используется


export type Action<
Type extends typeof actionNames[keyof typeof actionNames],
Payload = void
Expand Down
2 changes: 1 addition & 1 deletion lib/static/modules/load-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import classNames from 'classnames';
import * as SemanticUIReact from 'semantic-ui-react';
import ReactMarkdown from 'react-markdown';
import reduceReducers from 'reduce-reducers';
import immer from 'immer';
import {produce as immer} from 'immer';
import * as reselect from 'reselect';
import axios from 'axios';
import * as GravityUI from '@gravity-ui/uikit';
Expand Down
Loading
Loading