├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── 1-bug-report.yaml │ ├── 2-feature-request.yaml │ └── config.yml └── workflows │ ├── e2e.yml │ ├── node.js.yml │ ├── publish.yml │ └── testplane-reports-ttl.yml ├── .gitignore ├── .mocharc-jsdom.js ├── .mocharc.js ├── .npmignore ├── .nvmrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.json ├── bin └── html-reporter ├── docs ├── en │ ├── html-reporter-api.md │ ├── html-reporter-commands.md │ ├── html-reporter-custom-gui.md │ ├── html-reporter-events.md │ ├── html-reporter-jest.md │ ├── html-reporter-plugins.md │ ├── html-reporter-setup.md │ └── html-reporter.md ├── images │ ├── demo-gifs │ │ ├── analytics.gif │ │ ├── ci-and-local.gif │ │ ├── run-debug.gif │ │ └── visual-checks.gif │ ├── html-reporter-dark.svg │ ├── html-reporter-demo.png │ └── html-reporter-light.svg └── ru │ ├── html-reporter-api.md │ ├── html-reporter-commands.md │ ├── html-reporter-custom-gui.md │ ├── html-reporter-events.md │ ├── html-reporter-jest.md │ ├── html-reporter-plugins.md │ ├── html-reporter-setup.md │ └── html-reporter.md ├── hermione.ts ├── jest.ts ├── lib ├── adapters │ ├── config │ │ ├── index.ts │ │ ├── playwright.ts │ │ └── testplane.ts │ ├── event-handling │ │ └── testplane │ │ │ └── snapshots.ts │ ├── test-collection │ │ ├── index.ts │ │ ├── playwright.ts │ │ └── testplane.ts │ ├── test-result │ │ ├── hermione.ts │ │ ├── index.ts │ │ ├── jest.ts │ │ ├── playwright.ts │ │ ├── reporter.ts │ │ ├── sqlite.ts │ │ ├── testplane.ts │ │ ├── transformers │ │ │ ├── db.ts │ │ │ └── tree.ts │ │ └── utils │ │ │ └── index.ts │ ├── test │ │ ├── index.ts │ │ ├── playwright.ts │ │ └── testplane.ts │ └── tool │ │ ├── index.ts │ │ ├── playwright │ │ ├── index.ts │ │ ├── ipc.ts │ │ ├── reporter.ts │ │ └── transformer.ts │ │ ├── testplane │ │ ├── index.ts │ │ ├── runner │ │ │ ├── all-test-runner.ts │ │ │ ├── index.ts │ │ │ ├── runner.ts │ │ │ └── specific-test-runner.ts │ │ └── test-results-handler.ts │ │ └── types.ts ├── bundle │ ├── constants.ts │ ├── index.ts │ └── transformer.ts ├── cache.ts ├── cli │ ├── commands │ │ ├── gui.js │ │ ├── merge-reports.js │ │ └── remove-unused-screens │ │ │ ├── index.js │ │ │ └── utils.js │ └── index.ts ├── common-utils.ts ├── config │ ├── custom-gui-asserts.ts │ └── index.ts ├── constants │ ├── browser.ts │ ├── checked-statuses.ts │ ├── database.ts │ ├── defaults.ts │ ├── diff-modes.ts │ ├── errors.ts │ ├── expand-modes.js │ ├── extension-points.js │ ├── features.ts │ ├── group-tests.js │ ├── index.ts │ ├── local-storage.ts │ ├── paths.ts │ ├── performance-marks.ts │ ├── plugin-events.ts │ ├── save-formats.ts │ ├── test-statuses.ts │ ├── tests.ts │ ├── tool-names.ts │ └── view-modes.ts ├── db-utils │ ├── client.js │ ├── common.ts │ ├── migrations.ts │ └── server.ts ├── errors │ └── index.ts ├── gui │ ├── api │ │ ├── facade.ts │ │ └── index.ts │ ├── app.ts │ ├── constants │ │ ├── client-events.ts │ │ ├── custom-gui-control-types.ts │ │ ├── gui-events.ts │ │ ├── index.ts │ │ └── server.ts │ ├── event-source.ts │ ├── index.ts │ ├── routes │ │ └── plugins.ts │ ├── server.ts │ └── tool-runner │ │ ├── index.ts │ │ └── utils.ts ├── image-cache.ts ├── image-store.ts ├── images-info-saver.ts ├── local-image-file-saver.ts ├── merge-reports │ └── index.js ├── plugin-api.ts ├── plugin-utils.ts ├── report-builder │ ├── gui.ts │ └── static.ts ├── reporter-helpers.ts ├── server-utils.ts ├── sqlite-client.ts ├── static │ ├── .eslintrc.js │ ├── components │ │ ├── bottom-progress-bar │ │ │ └── index.jsx │ │ ├── bullet.jsx │ │ ├── controls │ │ │ ├── accept-opened-button.jsx │ │ │ ├── base-host-input.jsx │ │ │ ├── browser-list │ │ │ │ ├── index.jsx │ │ │ │ ├── index.styl │ │ │ │ └── utils.jsx │ │ │ ├── common-controls.jsx │ │ │ ├── common-filters.jsx │ │ │ ├── control-button.tsx │ │ │ ├── controls.less │ │ │ ├── custom-gui-controls.jsx │ │ │ ├── find-same-diffs-button.jsx │ │ │ ├── gui-controls.jsx │ │ │ ├── menu-bar.jsx │ │ │ ├── report-controls.jsx │ │ │ ├── report-info.jsx │ │ │ ├── run-button │ │ │ │ ├── index.jsx │ │ │ │ └── index.styl │ │ │ ├── selects │ │ │ │ ├── control.jsx │ │ │ │ ├── group-tests.jsx │ │ │ │ ├── index.styl │ │ │ │ └── label.jsx │ │ │ ├── show-checkboxes-input.jsx │ │ │ ├── strict-match-filter-input.jsx │ │ │ └── test-name-filter-input.jsx │ │ ├── details.jsx │ │ ├── error-boundary.js │ │ ├── extension-point.jsx │ │ ├── favicon-changer.js │ │ ├── group-tests │ │ │ ├── item.jsx │ │ │ ├── list.jsx │ │ │ └── prop-types.js │ │ ├── gui.jsx │ │ ├── header │ │ │ ├── header.css │ │ │ ├── index.jsx │ │ │ └── summary │ │ │ │ ├── dbBtn.jsx │ │ │ │ ├── dbSummary.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── item.jsx │ │ │ │ └── summary.css │ │ ├── icons │ │ │ ├── arrow.jsx │ │ │ ├── arrows-close.jsx │ │ │ ├── arrows-move.jsx │ │ │ ├── arrows-open.jsx │ │ │ └── view-in-browser │ │ │ │ ├── index.jsx │ │ │ │ └── index.styl │ │ ├── loading.jsx │ │ ├── main-tree.jsx │ │ ├── measurement-context.js │ │ ├── modals │ │ │ ├── find-same-diffs.jsx │ │ │ ├── index.js │ │ │ ├── modal.css │ │ │ ├── modal.jsx │ │ │ ├── screenshot-accepter │ │ │ │ ├── body.jsx │ │ │ │ ├── header.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── meta.jsx │ │ │ │ └── style.css │ │ │ └── static-accepter-confirm │ │ │ │ ├── index.tsx │ │ │ │ └── style.css │ │ ├── popup │ │ │ ├── index.jsx │ │ │ └── index.styl │ │ ├── progress-bar │ │ │ ├── index.jsx │ │ │ └── index.styl │ │ ├── prop-types.ts │ │ ├── report.jsx │ │ ├── retry-switcher │ │ │ └── index.jsx │ │ ├── section │ │ │ ├── body │ │ │ │ ├── description.jsx │ │ │ │ ├── history │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── index.styl │ │ │ │ ├── index.jsx │ │ │ │ ├── meta-info │ │ │ │ │ └── index.jsx │ │ │ │ ├── page-screenshot.tsx │ │ │ │ ├── result.jsx │ │ │ │ └── tabs.jsx │ │ │ ├── section-browser.jsx │ │ │ ├── section-common.jsx │ │ │ ├── title │ │ │ │ ├── browser-skipped.jsx │ │ │ │ ├── browser.jsx │ │ │ │ └── simple.jsx │ │ │ └── utils.js │ │ ├── state │ │ │ ├── error-details.jsx │ │ │ ├── index.jsx │ │ │ ├── state-error.jsx │ │ │ ├── state-fail │ │ │ │ ├── index.jsx │ │ │ │ └── index.styl │ │ │ └── state-success.jsx │ │ ├── sticky-header │ │ │ ├── gui.jsx │ │ │ ├── index.jsx │ │ │ ├── index.styl │ │ │ └── report.jsx │ │ └── suites.jsx │ ├── constants │ │ └── sort-tests.ts │ ├── containers │ │ ├── array │ │ │ ├── array.styl │ │ │ └── index.jsx │ │ └── modal.jsx │ ├── gui.css │ ├── gui.jsx │ ├── hooks │ │ ├── useElementSize.js │ │ ├── useEventListener.js │ │ ├── useLocalStorage.js │ │ └── useWindowSize.js │ ├── icons │ │ ├── broken-snapshot.svg │ │ ├── empty-report.svg │ │ ├── exclamation-triangle-large.svg │ │ ├── favicon-failure.png │ │ ├── favicon-running.png │ │ ├── favicon-success.png │ │ ├── favicon.png │ │ ├── github-icon.svg │ │ ├── testplane-mono-black.svg │ │ ├── testplane-mono.svg │ │ └── testplane.svg │ ├── index.jsx │ ├── modules │ │ ├── action-names.ts │ │ ├── actions │ │ │ ├── browsers.ts │ │ │ ├── custom-gui.ts │ │ │ ├── features.ts │ │ │ ├── filter-tests.ts │ │ │ ├── find-same-diffs.ts │ │ │ ├── group-tests.ts │ │ │ ├── gui-server-connection.ts │ │ │ ├── index.ts │ │ │ ├── lifecycle.ts │ │ │ ├── loading.ts │ │ │ ├── modals.ts │ │ │ ├── notifications.ts │ │ │ ├── processing.ts │ │ │ ├── run-tests.ts │ │ │ ├── screenshots.ts │ │ │ ├── settings.ts │ │ │ ├── snapshots.ts │ │ │ ├── sort-tests.ts │ │ │ ├── static-accepter.ts │ │ │ ├── suites-page.ts │ │ │ ├── suites-tree-state.ts │ │ │ ├── types.ts │ │ │ └── visual-checks-page.ts │ │ ├── custom-queries.js │ │ ├── default-state.ts │ │ ├── load-plugin.js │ │ ├── local-storage-wrapper.js │ │ ├── middlewares │ │ │ ├── local-storage.js │ │ │ └── metrika.ts │ │ ├── plugins.js │ │ ├── query-params.js │ │ ├── reducers │ │ │ ├── api-values.js │ │ │ ├── auto-run.js │ │ │ ├── bottom-progress-bar.js │ │ │ ├── browsers.js │ │ │ ├── close-ids.js │ │ │ ├── config.js │ │ │ ├── date.js │ │ │ ├── db.js │ │ │ ├── features.ts │ │ │ ├── fetch-db-details.js │ │ │ ├── grouped-tests │ │ │ │ ├── by │ │ │ │ │ ├── meta.js │ │ │ │ │ └── result.js │ │ │ │ ├── helpers.js │ │ │ │ └── index.js │ │ │ ├── gui-server-connection.ts │ │ │ ├── gui.js │ │ │ ├── index.js │ │ │ ├── is-initialized.js │ │ │ ├── loading.js │ │ │ ├── modals.js │ │ │ ├── new-ui-grouped-tests │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── notifications.js │ │ │ ├── plugins.js │ │ │ ├── processing.js │ │ │ ├── running.js │ │ │ ├── skips.js │ │ │ ├── snapshots.ts │ │ │ ├── sort-tests.ts │ │ │ ├── static-image-accepter.js │ │ │ ├── stats.ts │ │ │ ├── stopping.js │ │ │ ├── suites-page.ts │ │ │ ├── timestamp.js │ │ │ ├── tree │ │ │ │ ├── helpers.js │ │ │ │ ├── index.js │ │ │ │ └── nodes │ │ │ │ │ ├── browsers.js │ │ │ │ │ ├── images.js │ │ │ │ │ ├── results.js │ │ │ │ │ └── suites.js │ │ │ ├── view.js │ │ │ └── visual-checks-page.ts │ │ ├── selectors │ │ │ ├── grouped-tests.js │ │ │ ├── index.js │ │ │ ├── stats.js │ │ │ ├── tree.js │ │ │ └── view.js │ │ ├── static-image-accepter.ts │ │ ├── store.js │ │ ├── utils │ │ │ ├── imageEntity.ts │ │ │ ├── index.js │ │ │ ├── performance.ts │ │ │ └── state.ts │ │ ├── web-vitals.ts │ │ └── yandex-metrika.ts │ ├── new-ui.css │ ├── new-ui │ │ ├── app │ │ │ ├── App.tsx │ │ │ ├── gui.tsx │ │ │ ├── report.tsx │ │ │ └── selectors.ts │ │ ├── components │ │ │ ├── AdaptiveSelect │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── AsidePanel │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── AssertViewResult │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── AssertViewStatus │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── AttemptPickerItem │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Card │ │ │ │ ├── AnimatedAppearCard.module.css │ │ │ │ ├── AnimatedAppearCard.tsx │ │ │ │ ├── EmptyReportCard.module.css │ │ │ │ ├── EmptyReportCard.tsx │ │ │ │ ├── KeepDraggingToHideCard.module.css │ │ │ │ ├── KeepDraggingToHideCard.tsx │ │ │ │ ├── TextHintCard.module.css │ │ │ │ ├── TextHintCard.tsx │ │ │ │ ├── UiCard.module.css │ │ │ │ ├── UiCard.tsx │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── ChangedDot │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── CompactAttemptPicker │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── CustomScripts │ │ │ │ └── index.tsx │ │ │ ├── DiffViewer │ │ │ │ ├── ListMode.module.css │ │ │ │ ├── ListMode.tsx │ │ │ │ ├── OnionSkinMode.module.css │ │ │ │ ├── OnionSkinMode.tsx │ │ │ │ ├── OnlyDiffMode.tsx │ │ │ │ ├── SideBySideMode.module.css │ │ │ │ ├── SideBySideMode.tsx │ │ │ │ ├── SideBySideToFitMode.module.css │ │ │ │ ├── SideBySideToFitMode.tsx │ │ │ │ ├── SwipeMode.module.css │ │ │ │ ├── SwipeMode.tsx │ │ │ │ ├── SwitchMode.module.css │ │ │ │ ├── SwitchMode.tsx │ │ │ │ ├── common.module.css │ │ │ │ ├── index.module.css │ │ │ │ ├── index.tsx │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── ErrorInfo │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── GuiniToolbarOverlay │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── IconButton │ │ │ │ └── index.tsx │ │ │ ├── ImageLabel │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── ImageWithMagnifier │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── InfoPanel │ │ │ │ ├── DataSourceItem.module.css │ │ │ │ ├── DataSourceItem.tsx │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── LoadingBar │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── MainLayout │ │ │ │ ├── Footer.tsx │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── MetaInfo │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── MetrikaScript │ │ │ │ └── index.tsx │ │ │ ├── NamedSwitch │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── PanelSection │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Screenshot │ │ │ │ ├── DiffCircle.module.css │ │ │ │ ├── DiffCircle.tsx │ │ │ │ ├── index.module.css │ │ │ │ ├── index.tsx │ │ │ │ └── utils.ts │ │ │ ├── SettingsPanel │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── SplitViewLayout.module.css │ │ │ ├── SplitViewLayout.tsx │ │ │ ├── SuiteTitle │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── ToolbarOverlay │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── TreeViewItem │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── TreeViewItemIcon │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ └── UiModeHintNotification │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ ├── features │ │ │ ├── error-handling │ │ │ │ └── components │ │ │ │ │ └── ErrorHandling │ │ │ │ │ ├── Boundary.tsx │ │ │ │ │ ├── actions.tsx │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── fallbacks.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── interfaces.ts │ │ │ ├── info │ │ │ │ └── components │ │ │ │ │ └── InfoPage.tsx │ │ │ ├── suites │ │ │ │ ├── components │ │ │ │ │ ├── BrowsersSelect │ │ │ │ │ │ ├── BrowserIcon.module.css │ │ │ │ │ │ ├── BrowserIcon.tsx │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── CollapsibleSection │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── GroupBySelect │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ScreenshotsTreeViewItem │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── SnapshotsPlayer │ │ │ │ │ │ ├── PlayIcon.tsx │ │ │ │ │ │ ├── Timeline.module.css │ │ │ │ │ │ ├── Timeline.tsx │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── SortBySelect │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── SuitesPage │ │ │ │ │ │ ├── TestInfoSkeleton.tsx │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── types.ts │ │ │ │ │ ├── SuitesTreeView │ │ │ │ │ │ ├── TreeViewSkeleton.module.css │ │ │ │ │ │ ├── TreeViewSkeleton.tsx │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── selectors.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── TestControlPanel │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── TestInfo │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── TestNameFilter │ │ │ │ │ │ ├── TestNameFilterButton.tsx │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── TestStatusFilter │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── selectors.ts │ │ │ │ │ ├── TestStepArgs │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── TestSteps │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── selectors.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── TreeActionsToolbar │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── TreeViewItemSubtitle │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── TreeViewItemTitle │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── selectors.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── utils.ts │ │ │ └── visual-checks │ │ │ │ ├── components │ │ │ │ └── VisualChecksPage │ │ │ │ │ ├── AssertViewResultSkeleton.tsx │ │ │ │ │ ├── VisualChecksStickyHeader.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ └── selectors.ts │ │ ├── hooks │ │ │ └── useAnalytics.ts │ │ ├── providers │ │ │ ├── analytics.tsx │ │ │ └── event-source.tsx │ │ ├── store │ │ │ └── selectors.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ ├── store.ts │ │ │ └── typings.d.ts │ │ └── utils │ │ │ ├── analytics.ts │ │ │ ├── api.ts │ │ │ ├── assert-view-status.tsx │ │ │ └── index.tsx │ ├── styles.css │ ├── template-new-ui.html │ ├── template.html │ ├── tsconfig.json │ └── variables.css ├── test-attempt-manager.ts ├── tests-tree-builder │ ├── base.ts │ ├── gui.ts │ └── static.ts ├── types.ts └── workers │ ├── create-workers.ts │ └── worker.ts ├── package-lock.json ├── package.json ├── playwright.ts ├── test ├── .eslintrc.js ├── func │ ├── .eslintrc.js │ ├── common.testplane.conf.js │ ├── docker │ │ ├── .dockerignore │ │ └── Dockerfile │ ├── fixtures │ │ ├── analytics │ │ │ ├── disabled.testplane.conf.js │ │ │ ├── enabled.testplane.conf.js │ │ │ ├── package.json │ │ │ └── test.testplane.js │ │ ├── db-migrations │ │ │ ├── .testplane.conf.js │ │ │ ├── failed-describe.testplane.js │ │ │ ├── package.json │ │ │ ├── report │ │ │ │ ├── data.js │ │ │ │ ├── databaseUrls.json │ │ │ │ ├── icons │ │ │ │ │ ├── broken-snapshot.svg │ │ │ │ │ ├── empty-report.svg │ │ │ │ │ ├── exclamation-triangle-large.svg │ │ │ │ │ ├── favicon-failure.png │ │ │ │ │ ├── favicon-running.png │ │ │ │ │ ├── favicon-success.png │ │ │ │ │ ├── favicon.png │ │ │ │ │ ├── github-icon.svg │ │ │ │ │ ├── testplane-mono-black.svg │ │ │ │ │ ├── testplane-mono.svg │ │ │ │ │ └── testplane.svg │ │ │ │ ├── index.html │ │ │ │ ├── new-ui.html │ │ │ │ ├── newReport.min.css │ │ │ │ ├── newReport.min.js │ │ │ │ ├── report.min.css │ │ │ │ ├── report.min.js │ │ │ │ ├── sql-wasm.js │ │ │ │ ├── sql-wasm.wasm │ │ │ │ └── sqlite.db │ │ │ └── success-describe.testplane.js │ │ ├── fixtures.testplane.conf.js │ │ ├── playwright │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── playwright.config.ts │ │ │ └── tests │ │ │ │ ├── failed-describe.spec.ts │ │ │ │ ├── screens │ │ │ │ ├── failed-describe-test-with-diff │ │ │ │ │ └── chromium │ │ │ │ │ │ └── header.png │ │ │ │ ├── failed-describe-test-with-image-comparison-diff │ │ │ │ │ └── chromium │ │ │ │ │ │ └── header.png │ │ │ │ ├── failed-describe-test-with-successful-assertView-and-error │ │ │ │ │ └── chromium │ │ │ │ │ │ └── header-success.png │ │ │ │ ├── failed-describe-test-without-screenshot │ │ │ │ │ └── chromium │ │ │ │ │ │ └── header.png │ │ │ │ └── success-describe-test-with-screenshot │ │ │ │ │ └── chromium │ │ │ │ │ └── header.png │ │ │ │ └── success-describe.spec.ts │ │ ├── plugins │ │ │ ├── .testplane.conf.js │ │ │ ├── failed-describe.testplane.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── screens │ │ │ │ └── eea1754 │ │ │ │ │ └── chrome │ │ │ │ │ └── header.png │ │ │ └── success-describe.testplane.js │ │ ├── testplane-eye │ │ │ ├── .testplane.conf.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ └── test.testplane.js │ │ ├── testplane-gui │ │ │ ├── .testplane.conf.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── screens │ │ │ │ └── 442f53a │ │ │ │ │ └── chrome │ │ │ │ │ └── paragraph.png │ │ │ └── test.testplane.js │ │ ├── testplane-tinder │ │ │ ├── .testplane.conf.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── screens │ │ │ │ └── 442f53a │ │ │ │ │ └── chrome │ │ │ │ │ └── paragraph.png │ │ │ └── test.testplane.js │ │ └── testplane │ │ │ ├── .testplane.conf.js │ │ │ ├── failed-describe.testplane.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── screens │ │ │ ├── 7357338 │ │ │ │ └── chrome │ │ │ │ │ └── header.png │ │ │ ├── ba3c69a │ │ │ │ └── chrome │ │ │ │ │ └── header.png │ │ │ ├── eea1754 │ │ │ │ └── chrome │ │ │ │ │ └── header.png │ │ │ └── f0c3ac2 │ │ │ │ └── chrome │ │ │ │ └── header.png │ │ │ └── success-describe.testplane.js │ ├── packages │ │ ├── .eslintrc.js │ │ ├── basic │ │ │ ├── lib │ │ │ │ ├── color-border.css │ │ │ │ └── color-border.jsx │ │ │ ├── package.json │ │ │ └── webpack.config.js │ │ ├── html-reporter-test-server │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── html-reporter-tester │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ └── playwright.js │ │ ├── menu-bar │ │ │ ├── lib │ │ │ │ ├── menu-bar-item.css │ │ │ │ └── menu-bar-item.jsx │ │ │ ├── package.json │ │ │ └── webpack.config.js │ │ ├── redux-with-server │ │ │ ├── lib │ │ │ │ ├── color-border.css │ │ │ │ └── color-border.jsx │ │ │ ├── middleware.js │ │ │ ├── package.json │ │ │ └── webpack.config.js │ │ ├── redux │ │ │ ├── lib │ │ │ │ ├── color-border.css │ │ │ │ └── color-border.jsx │ │ │ ├── package.json │ │ │ └── webpack.config.js │ │ └── webpack.common.js │ ├── tests │ │ ├── .testplane.conf.js │ │ ├── analytics │ │ │ └── index.testplane.js │ │ ├── common-gui │ │ │ └── index.testplane.js │ │ ├── common-tinder │ │ │ └── index.testplane.js │ │ ├── common │ │ │ ├── error-group.testplane.js │ │ │ ├── new-ui │ │ │ │ └── suites-page │ │ │ │ │ └── expand-collapse-button.testplane.js │ │ │ ├── test-results-appearance.testplane.js │ │ │ ├── tests-details.testplane.js │ │ │ └── tests-header.testplane.js │ │ ├── db-migrations │ │ │ └── index.testplane.js │ │ ├── eye │ │ │ └── index.testplane.js │ │ ├── local.testplane.conf.js │ │ ├── package.json │ │ ├── plugins │ │ │ ├── tests-basic-plugin.testplane.js │ │ │ ├── tests-menu-bar-plugin.testplane.js │ │ │ └── tests-redux-plugin.testplane.js │ │ ├── screens │ │ │ ├── 3144090 │ │ │ │ └── chrome │ │ │ │ │ └── menu bar plugins clicked.png │ │ │ ├── 9679255 │ │ │ │ └── chrome │ │ │ │ │ ├── button.png │ │ │ │ │ └── tooltip.png │ │ │ ├── 0049570 │ │ │ │ └── chrome │ │ │ │ │ └── retry-switcher.png │ │ │ ├── 07c99c0 │ │ │ │ └── chrome │ │ │ │ │ └── retry-switcher.png │ │ │ ├── 1bb949f │ │ │ │ └── chrome │ │ │ │ │ └── retry-switcher.png │ │ │ ├── 2df3350 │ │ │ │ └── chrome │ │ │ │ │ └── retry-selector.png │ │ │ ├── 45b9477 │ │ │ │ └── chrome │ │ │ │ │ └── retry-switcher.png │ │ │ ├── 5c90021 │ │ │ │ └── chrome │ │ │ │ │ └── basic plugins.png │ │ │ ├── 6551ff5 │ │ │ │ └── chrome │ │ │ │ │ ├── button.png │ │ │ │ │ └── tooltip.png │ │ │ ├── 67cd8d8 │ │ │ │ └── chrome │ │ │ │ │ └── retry-switcher.png │ │ │ ├── 683f0cf │ │ │ │ └── chrome │ │ │ │ │ └── retry-selector.png │ │ │ ├── 696c6c6 │ │ │ │ └── chrome │ │ │ │ │ └── details summary.png │ │ │ ├── 6a4b847 │ │ │ │ └── chrome │ │ │ │ │ └── retry-selector.png │ │ │ ├── 972e9ff │ │ │ │ └── chrome │ │ │ │ │ └── menu bar plugins.png │ │ │ ├── a8c2699 │ │ │ │ └── chrome │ │ │ │ │ └── retry-selector.png │ │ │ ├── bb1ceae │ │ │ │ └── chrome │ │ │ │ │ └── details summary.png │ │ │ ├── bc098ae │ │ │ │ └── chrome │ │ │ │ │ └── retry-selector.png │ │ │ ├── bdf4a21 │ │ │ │ └── chrome │ │ │ │ │ └── retry-switcher.png │ │ │ ├── be4ff5b │ │ │ │ └── chrome │ │ │ │ │ └── basic plugins clicked.png │ │ │ ├── befc47b │ │ │ │ └── chrome │ │ │ │ │ └── retry-selector.png │ │ │ ├── cfcb171 │ │ │ │ └── chrome │ │ │ │ │ └── retry-selector.png │ │ │ ├── d8c5b8a │ │ │ │ └── chrome │ │ │ │ │ └── redux plugin clicked.png │ │ │ ├── e219988 │ │ │ │ └── chrome │ │ │ │ │ └── section.png │ │ │ ├── f23f882 │ │ │ │ └── chrome │ │ │ │ │ └── retry-selector.png │ │ │ └── f8fe878 │ │ │ │ └── chrome │ │ │ │ ├── button.png │ │ │ │ └── tooltip.png │ │ ├── static-server.js │ │ └── utils.js │ └── utils │ │ ├── constants.js │ │ ├── get-port.js │ │ └── index.js ├── setup │ ├── assert-ext.js │ ├── configure-testing-library.js │ ├── css-modules-mock.js │ ├── globals.js │ ├── jsdom.js │ └── ts-node.js ├── tsconfig.json ├── types.ts └── unit │ ├── lib │ ├── adapters │ │ ├── config │ │ │ ├── playwright.ts │ │ │ └── testplane.ts │ │ ├── test-collection │ │ │ ├── playwright.ts │ │ │ └── testplane.ts │ │ ├── test-result │ │ │ ├── jest.ts │ │ │ ├── playwright.ts │ │ │ └── testplane.ts │ │ ├── test │ │ │ ├── playwright.ts │ │ │ └── testplane.ts │ │ └── tool │ │ │ ├── playwright │ │ │ └── index.ts │ │ │ └── testplane │ │ │ ├── index.ts │ │ │ └── test-results-handler.js │ ├── cli │ │ └── commands │ │ │ └── remove-unused-screens │ │ │ ├── index.js │ │ │ └── utils.js │ ├── common-utils.ts │ ├── config │ │ └── index.js │ ├── db-utils │ │ ├── client.js │ │ └── migrations.ts │ ├── gui │ │ ├── api │ │ │ └── index.js │ │ ├── app.js │ │ ├── routes │ │ │ └── plugins.js │ │ ├── server.js │ │ └── tool-runner │ │ │ ├── index.js │ │ │ └── utils.ts │ ├── images-info-saver.ts │ ├── local-image-file-saver.js │ ├── merge-reports │ │ └── index.js │ ├── plugin-api.js │ ├── plugin-utils.js │ ├── report-builder │ │ ├── gui.js │ │ └── static.js │ ├── server-utils.js │ ├── sqlite-client.js │ ├── static │ │ ├── components │ │ │ ├── .eslintrc.js │ │ │ ├── bottom-progress-bar │ │ │ │ └── index.jsx │ │ │ ├── bullet.jsx │ │ │ ├── controls │ │ │ │ ├── accept-opened-button.jsx │ │ │ │ ├── browser-list │ │ │ │ │ └── index.jsx │ │ │ │ ├── custom-gui-controls.jsx │ │ │ │ ├── find-same-diffs-button.jsx │ │ │ │ ├── gui-controls.jsx │ │ │ │ ├── menu-bar.jsx │ │ │ │ ├── run-button.jsx │ │ │ │ ├── show-checkboxes-input.jsx │ │ │ │ └── strict-match-filter-input.jsx │ │ │ ├── custom-scripts.tsx │ │ │ ├── details.jsx │ │ │ ├── error-boundary.jsx │ │ │ ├── extension-point.jsx │ │ │ ├── group-tests │ │ │ │ └── item.jsx │ │ │ ├── modals │ │ │ │ └── screenshot-accepter │ │ │ │ │ ├── body.jsx │ │ │ │ │ ├── header.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── meta.jsx │ │ │ ├── retry-switcher │ │ │ │ └── index.jsx │ │ │ ├── section │ │ │ │ ├── body │ │ │ │ │ ├── description.jsx │ │ │ │ │ ├── history.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── meta-info │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── result.jsx │ │ │ │ │ └── tabs.jsx │ │ │ │ ├── section-browser.jsx │ │ │ │ ├── section-common.jsx │ │ │ │ └── title │ │ │ │ │ ├── browser-skipped.jsx │ │ │ │ │ ├── browser.jsx │ │ │ │ │ └── simple.jsx │ │ │ ├── state │ │ │ │ ├── index.jsx │ │ │ │ └── state-error.jsx │ │ │ └── suites.jsx │ │ ├── modules │ │ │ ├── actions │ │ │ │ ├── custom-gui.ts │ │ │ │ ├── index.js │ │ │ │ ├── lifecycle.ts │ │ │ │ └── run-tests.ts │ │ │ ├── custom-queries.js │ │ │ ├── load-plugin.js │ │ │ ├── local-storage-wrapper.js │ │ │ ├── middlewares │ │ │ │ ├── local-storage.js │ │ │ │ └── metrika.js │ │ │ ├── plugins.js │ │ │ ├── query-params.js │ │ │ ├── reducers │ │ │ │ ├── bottom-progress-bar.js │ │ │ │ ├── grouped-tests │ │ │ │ │ ├── by │ │ │ │ │ │ ├── meta.js │ │ │ │ │ │ └── result.js │ │ │ │ │ ├── helpers.js │ │ │ │ │ └── index.js │ │ │ │ ├── modals.js │ │ │ │ ├── plugins.js │ │ │ │ ├── processing.js │ │ │ │ ├── static-image-accepter.ts │ │ │ │ ├── stats.ts │ │ │ │ ├── tree │ │ │ │ │ └── index.js │ │ │ │ └── view.js │ │ │ ├── selectors │ │ │ │ ├── grouped-tests.js │ │ │ │ ├── stats.js │ │ │ │ └── tree.js │ │ │ ├── utils.js │ │ │ ├── web-vitals.js │ │ │ └── yandex-metrika.js │ │ ├── new-ui │ │ │ ├── components │ │ │ │ ├── AttemptPickerItem.tsx │ │ │ │ ├── MetaInfo.tsx │ │ │ │ └── Screenshot │ │ │ │ │ ├── DiffCircle.tsx │ │ │ │ │ └── index.tsx │ │ │ └── features │ │ │ │ ├── suites │ │ │ │ └── components │ │ │ │ │ └── SuitesTreeView │ │ │ │ │ └── utils.ts │ │ │ │ └── visual-checks │ │ │ │ └── components │ │ │ │ └── VisualChecksStickyHeader.jsx │ │ ├── state-utils.ts │ │ ├── tsconfig.json │ │ └── utils.tsx │ └── tests-tree-builder │ │ ├── base.js │ │ ├── gui.js │ │ └── static.js │ ├── testplane.js │ ├── utils.js │ └── workers │ └── worker.js ├── testplane.ts ├── tsconfig.common.json ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.eslintignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | build 3 | node_modules 4 | hot 5 | /lib/static/*.min.js 6 | /lib/static/data.js 7 | /lib/static/sql-wasm.js 8 | /@ 9 | /test/func/**/report 10 | /test/func/**/report-backup 11 | /test/func/**/reports 12 | /test/func/packages/*/plugin.js 13 | /hermione-report 14 | /testplane-report 15 | tmp 16 | **/playwright-report 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Question 🤷 4 | url: https://github.com/gemini-testing/html-reporter/discussions 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x, 22.6] # https://github.com/nodejs/node/issues/54532 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - run: npm ci 25 | - run: npm test 26 | 27 | - name: Build 28 | if: ${{ matrix.node-version == '20.x' }} 29 | run: npm run build 30 | 31 | - name: Publish 32 | if: ${{ matrix.node-version == '20.x' }} 33 | run: npx pkg-pr-new publish 34 | -------------------------------------------------------------------------------- /.github/workflows/testplane-reports-ttl.yml: -------------------------------------------------------------------------------- 1 | name: Remove old Testplane html reports 2 | on: 3 | schedule: # Runs once daily 4 | - cron: 0 0 * * * 5 | permissions: 6 | contents: write 7 | jobs: 8 | clean: 9 | name: Remove old Testplane html reports 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | ref: gh-pages 16 | token: ${{ secrets.GH_ACCESS_TOKEN }} 17 | - name: Remove reports 18 | uses: gemini-testing/gh-actions-reports-ttl-cleaner@v1 19 | with: 20 | html-report-prefix: testplane-reports 21 | ttl: 30 22 | user-name: y-infra 23 | user-email: y-infra@yandex.ru 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | hot 4 | sqlite.db 5 | *.log 6 | /lib/static/*.min.* 7 | /lib/static/*.eot 8 | /lib/static/*.ttf 9 | /lib/static/*.svg 10 | /lib/static/data.js 11 | /lib/static/images 12 | /lib/static/gui.html 13 | /lib/static/index.html 14 | /lib/static/sql-wasm.js 15 | /lib/static/sqlite.db 16 | /lib/static/databaseUrls.json 17 | 18 | .idea 19 | .vscode 20 | .DS_Store 21 | .nyc_output 22 | tmp 23 | **/.testplane 24 | 25 | **/playwright-report 26 | hermione-report 27 | testplane-report 28 | test/func/**/report 29 | test/func/**/report-backup 30 | test/func/**/reports 31 | test/func/packages/*/plugin.js 32 | test/func/fixtures/playwright/test-results 33 | @ 34 | -------------------------------------------------------------------------------- /.mocharc-jsdom.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | extension: ["js", "jsx", "ts", "tsx"], 5 | recursive: true, 6 | require: [ 7 | "./test/setup/ts-node", 8 | "./test/setup/jsdom", 9 | "./test/setup/globals", 10 | "./test/setup/assert-ext", 11 | "./test/setup/configure-testing-library" 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | extension: ["js", "jsx", "ts", "tsx"], 5 | recursive: true, 6 | require: [ 7 | "./test/setup/ts-node", 8 | "./test/setup/globals", 9 | "./test/setup/assert-ext", 10 | "./test/setup/configure-testing-library" 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | /test 3 | hot 4 | lib/static/js/ 5 | lib/static/report.css 6 | 7 | hermione-report 8 | testplane-report 9 | .hermione.conf.js 10 | .testplane.conf.js 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 YANDEX LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@babel/plugin-transform-runtime"] 4 | ], 5 | "presets": [ 6 | "@babel/preset-react", 7 | ["@babel/preset-env", { "modules": "auto"}], 8 | "@babel/preset-typescript" 9 | ], 10 | "sourceMaps": true, 11 | "retainLines": true 12 | } 13 | -------------------------------------------------------------------------------- /bin/html-reporter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | (async () => { 5 | await require('../build/lib/cli').run(); 6 | })(); 7 | -------------------------------------------------------------------------------- /docs/en/html-reporter.md: -------------------------------------------------------------------------------- 1 | # html-reporter 2 | 3 | * [Setup](./html-reporter-setup.md) 4 | * [Commands](./html-reporter-commands.md) 5 | * [Plugins](./html-reporter-plugins.md) 6 | * [Customizing GUI](./html-reporter-custom-gui.md) 7 | * [API](./html-reporter-api.md) 8 | * [Events](./html-reporter-events.md) 9 | -------------------------------------------------------------------------------- /docs/images/demo-gifs/analytics.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/docs/images/demo-gifs/analytics.gif -------------------------------------------------------------------------------- /docs/images/demo-gifs/ci-and-local.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/docs/images/demo-gifs/ci-and-local.gif -------------------------------------------------------------------------------- /docs/images/demo-gifs/run-debug.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/docs/images/demo-gifs/run-debug.gif -------------------------------------------------------------------------------- /docs/images/demo-gifs/visual-checks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/docs/images/demo-gifs/visual-checks.gif -------------------------------------------------------------------------------- /docs/images/html-reporter-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/docs/images/html-reporter-demo.png -------------------------------------------------------------------------------- /docs/ru/html-reporter.md: -------------------------------------------------------------------------------- 1 | # html-reporter 2 | 3 | * [Подключение](./html-reporter-setup.md) 4 | * [Команды](./html-reporter-commands.md) 5 | * [Плагины](./html-reporter-plugins.md) 6 | * [Кастомизация GUI](./html-reporter-custom-gui.md) 7 | * [API](./html-reporter-api.md) 8 | * [События](./html-reporter-events.md) 9 | -------------------------------------------------------------------------------- /hermione.ts: -------------------------------------------------------------------------------- 1 | import pluginHandler from './testplane'; 2 | 3 | module.exports = pluginHandler; 4 | -------------------------------------------------------------------------------- /lib/adapters/config/index.ts: -------------------------------------------------------------------------------- 1 | import {TestAdapter} from '../test'; 2 | 3 | export interface ConfigAdapter { 4 | readonly tolerance: number; 5 | readonly antialiasingTolerance: number; 6 | readonly browserIds: string[]; 7 | 8 | getScreenshotPath(test: TestAdapter, stateName: string): string; 9 | } 10 | -------------------------------------------------------------------------------- /lib/adapters/test-collection/index.ts: -------------------------------------------------------------------------------- 1 | import type {TestAdapter} from '../test'; 2 | 3 | export interface TestCollectionAdapter { 4 | readonly tests: TestAdapter[]; 5 | } 6 | -------------------------------------------------------------------------------- /lib/adapters/test-collection/playwright.ts: -------------------------------------------------------------------------------- 1 | import {PlaywrightTestAdapter, type PwtRawTest} from '../test/playwright'; 2 | import type {TestCollectionAdapter} from './'; 3 | 4 | export class PlaywrightTestCollectionAdapter implements TestCollectionAdapter { 5 | private _testAdapters: PlaywrightTestAdapter[]; 6 | 7 | static create( 8 | this: new (tests: PwtRawTest[]) => T, 9 | tests: PwtRawTest[] 10 | ): T { 11 | return new this(tests); 12 | } 13 | 14 | constructor(tests: PwtRawTest[]) { 15 | this._testAdapters = tests.map(test => PlaywrightTestAdapter.create(test)); 16 | } 17 | 18 | get tests(): PlaywrightTestAdapter[] { 19 | return this._testAdapters; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/adapters/test-collection/testplane.ts: -------------------------------------------------------------------------------- 1 | import {TestplaneTestAdapter} from '../test/testplane'; 2 | import type {TestCollectionAdapter} from './'; 3 | import type {TestCollection} from 'testplane'; 4 | 5 | export class TestplaneTestCollectionAdapter implements TestCollectionAdapter { 6 | private _testCollection: TestCollection; 7 | private _testAdapters: TestplaneTestAdapter[]; 8 | 9 | static create( 10 | this: new (testCollection: TestCollection) => T, 11 | testCollection: TestCollection 12 | ): T { 13 | return new this(testCollection); 14 | } 15 | 16 | constructor(testCollection: TestCollection) { 17 | this._testCollection = testCollection; 18 | this._testAdapters = this._testCollection.mapTests(test => TestplaneTestAdapter.create(test)); 19 | } 20 | 21 | get original(): TestCollection { 22 | return this._testCollection; 23 | } 24 | 25 | get tests(): TestplaneTestAdapter[] { 26 | return this._testAdapters; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/adapters/test-result/hermione.ts: -------------------------------------------------------------------------------- 1 | export { 2 | TestplaneTestResultAdapter as HermioneTestResultAdapter, 3 | TestplaneTestResultAdapterOptions as HermioneTestResultAdapterOptions, 4 | getStatus 5 | } from './testplane'; 6 | -------------------------------------------------------------------------------- /lib/adapters/test-result/transformers/tree.ts: -------------------------------------------------------------------------------- 1 | import {ReporterTestResult} from '../index'; 2 | import _ from 'lodash'; 3 | import {BaseTreeTestResult} from '../../../tests-tree-builder/base'; 4 | import {DbTestResultTransformer} from './db'; 5 | 6 | interface Options { 7 | baseHost?: string; 8 | } 9 | 10 | export class TreeTestResultTransformer { 11 | private _transformer: DbTestResultTransformer; 12 | 13 | constructor(options: Options) { 14 | this._transformer = new DbTestResultTransformer(options); 15 | } 16 | 17 | transform(testResult: ReporterTestResult): BaseTreeTestResult { 18 | const result = this._transformer.transform(testResult); 19 | 20 | return { 21 | ..._.omit(result, 'imagesInfo'), 22 | attempt: testResult.attempt, 23 | errorDetails: testResult.errorDetails 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/adapters/test/index.ts: -------------------------------------------------------------------------------- 1 | import {TestStatus} from '../../constants'; 2 | import type {ReporterTestResult} from '../test-result'; 3 | import type {AssertViewResult} from '../../types'; 4 | 5 | export interface CreateTestResultOpts { 6 | status: TestStatus; 7 | attempt?: number; 8 | assertViewResults?: AssertViewResult[]; 9 | error?: Error; 10 | sessionId?: string; 11 | meta?: { 12 | url?: string; 13 | }; 14 | duration: number; 15 | } 16 | 17 | export interface TestAdapter { 18 | readonly id: string; 19 | readonly pending: boolean; 20 | readonly disabled: boolean; 21 | readonly silentlySkipped: boolean; 22 | readonly browserId: string; 23 | readonly fullName: string; 24 | readonly file: string; 25 | readonly titlePath: string[]; 26 | 27 | createTestResult(opts: CreateTestResultOpts): ReporterTestResult; 28 | } 29 | -------------------------------------------------------------------------------- /lib/adapters/tool/playwright/ipc.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import type {ChildProcess} from 'node:child_process'; 3 | 4 | export default { 5 | emit: (event: string, data: Record = {}): void => { 6 | process.send && process.send({event, ...data}); 7 | }, 8 | on: >(event: string, handler: (msg: T & {event: string}) => void, proc: ChildProcess | NodeJS.Process = process): void => { 9 | proc.on('message', (msg: T & {event: string}) => { 10 | if (event !== _.get(msg, 'event')) { 11 | return; 12 | } 13 | 14 | handler(msg); 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/adapters/tool/playwright/transformer.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | export const setupTransformHook: () => VoidFunction = require('../../../bundle').setupTransformHook; 3 | -------------------------------------------------------------------------------- /lib/adapters/tool/testplane/runner/all-test-runner.ts: -------------------------------------------------------------------------------- 1 | import {BaseRunner} from './runner'; 2 | import type {TestCollection} from 'testplane'; 3 | 4 | export class AllTestRunner extends BaseRunner { 5 | override run(runHandler: (testCollection: TestCollection) => U): U { 6 | this._collection.enableAll(); 7 | 8 | return super.run(runHandler); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/adapters/tool/testplane/runner/index.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import type {TestCollection} from 'testplane'; 3 | 4 | import {AllTestRunner} from './all-test-runner'; 5 | import {SpecificTestRunner} from './specific-test-runner'; 6 | import type {TestRunner} from './runner'; 7 | import type {TestSpec} from '../../types'; 8 | 9 | export const createTestRunner = (collection: TestCollection, tests: TestSpec[]): TestRunner => { 10 | return _.isEmpty(tests) 11 | ? new AllTestRunner(collection) 12 | : new SpecificTestRunner(collection, tests); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/adapters/tool/testplane/runner/runner.ts: -------------------------------------------------------------------------------- 1 | import type {TestCollection} from 'testplane'; 2 | 3 | export interface TestRunner { 4 | run(handler: (testCollection: TestCollection) => U): U; 5 | } 6 | 7 | export class BaseRunner implements TestRunner { 8 | protected _collection: TestCollection; 9 | 10 | constructor(collection: TestCollection) { 11 | this._collection = collection; 12 | } 13 | 14 | run(runHandler: (testCollection: TestCollection) => U): U { 15 | return runHandler(this._collection); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/adapters/tool/testplane/runner/specific-test-runner.ts: -------------------------------------------------------------------------------- 1 | import {BaseRunner} from './runner'; 2 | import type {TestSpec} from '../../types'; 3 | import type {TestCollection} from 'testplane'; 4 | 5 | export class SpecificTestRunner extends BaseRunner { 6 | private _tests: TestSpec[]; 7 | 8 | constructor(collection: TestCollection, tests: TestSpec[]) { 9 | super(collection); 10 | 11 | this._tests = tests; 12 | } 13 | 14 | override run(runHandler: (testCollection: TestCollection) => U): U { 15 | this._filter(); 16 | 17 | return super.run(runHandler); 18 | } 19 | 20 | private _filter(): void { 21 | this._collection.disableAll(); 22 | 23 | this._tests.forEach(({testName, browserName}) => { 24 | this._collection.enableTest(testName, browserName); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/adapters/tool/types.ts: -------------------------------------------------------------------------------- 1 | export interface TestSpec { 2 | testName: string; 3 | browserName: string; 4 | } 5 | 6 | export interface CustomGuiActionPayload { 7 | sectionName: string; 8 | groupIndex: number; 9 | controlIndex: number; 10 | } 11 | -------------------------------------------------------------------------------- /lib/bundle/constants.ts: -------------------------------------------------------------------------------- 1 | export const TRANSFORM_EXTENSIONS = ['.ts', '.mts', '.mjs']; 2 | -------------------------------------------------------------------------------- /lib/bundle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transformer'; 2 | export * from './constants'; 3 | -------------------------------------------------------------------------------- /lib/bundle/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as babel from '@babel/core'; 2 | import {addHook} from 'pirates'; 3 | import {TRANSFORM_EXTENSIONS} from './constants'; 4 | 5 | import type {TransformOptions} from '@babel/core'; 6 | 7 | export const setupTransformHook = (): VoidFunction => { 8 | const transformOptions: TransformOptions = { 9 | browserslistConfigFile: false, 10 | babelrc: false, 11 | configFile: false, 12 | compact: false, 13 | presets: [require('@babel/preset-typescript')], 14 | plugins: [ 15 | require('@babel/plugin-transform-modules-commonjs') 16 | ] 17 | }; 18 | 19 | const revertTransformHook = addHook( 20 | (originalCode, filename) => { 21 | return babel.transform(originalCode, {filename, ...transformOptions})?.code as string; 22 | }, 23 | {exts: TRANSFORM_EXTENSIONS} 24 | ); 25 | 26 | return revertTransformHook; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/cache.ts: -------------------------------------------------------------------------------- 1 | export class Cache { 2 | private _getKeyHash: (key: Key) => string; 3 | private _cache: Map; 4 | 5 | constructor(hashFn: (key: Key) => string) { 6 | this._getKeyHash = hashFn; 7 | this._cache = new Map(); 8 | } 9 | 10 | has(key: Key): boolean { 11 | const keyHash = this._getKeyHash(key); 12 | 13 | return this._cache.has(keyHash); 14 | } 15 | 16 | get(key: Key): Value | undefined { 17 | const keyHash = this._getKeyHash(key); 18 | 19 | return this._cache.get(keyHash); 20 | } 21 | 22 | set(key: Key, value: Value): this { 23 | const keyHash = this._getKeyHash(key); 24 | 25 | if (value !== undefined) { 26 | this._cache.set(keyHash, value); 27 | } else { 28 | this._cache.delete(keyHash); 29 | } 30 | 31 | return this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/cli/commands/gui.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {commands} = require('..'); 4 | const runGui = require('../../gui').default; 5 | 6 | const {GUI: commandName} = commands; 7 | 8 | module.exports = (cliTool, toolAdapter) => { 9 | // must be executed here because it adds `gui` field in tool instance, 10 | // which is available to other plugins and is an API for interacting with the current plugin 11 | toolAdapter.initGuiApi(); 12 | 13 | cliTool 14 | .command(`${commandName} [paths...]`) 15 | .allowUnknownOption() 16 | .description('update the changed screenshots or gather them if they does not exist') 17 | .option('-p, --port ', 'Port to launch server on', 8000) 18 | .option('--hostname ', 'Hostname to launch server on', 'localhost') 19 | .option('-a, --auto-run', 'auto run immediately') 20 | .option('-O, --no-open', 'not to open a browser window after starting the server') 21 | .action((paths, options) => { 22 | runGui({paths, toolAdapter, cli: {options, tool: cliTool}}); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/constants/browser.ts: -------------------------------------------------------------------------------- 1 | export enum BrowserVersions { 2 | UNKNOWN = 'unknown' 3 | } 4 | 5 | export enum BrowserFeature { 6 | LiveSnapshotsStreaming = 'live-snapshots-streaming', 7 | } 8 | -------------------------------------------------------------------------------- /lib/constants/checked-statuses.ts: -------------------------------------------------------------------------------- 1 | export const UNCHECKED = 0; 2 | export const INDETERMINATE = 0.5; 3 | export const CHECKED = 1; 4 | 5 | export default { 6 | UNCHECKED, 7 | INDETERMINATE, 8 | CHECKED 9 | }; 10 | 11 | export type CheckStatus = typeof UNCHECKED | typeof INDETERMINATE | typeof CHECKED; 12 | -------------------------------------------------------------------------------- /lib/constants/defaults.ts: -------------------------------------------------------------------------------- 1 | import {DiffModes} from './diff-modes'; 2 | import {ViewMode} from './view-modes'; 3 | import {StoreReporterConfig} from '../types'; 4 | import {SaveFormat} from './save-formats'; 5 | 6 | export const CIRCLE_RADIUS = 150; 7 | 8 | export const configDefaults: StoreReporterConfig = { 9 | baseHost: '', 10 | commandsWithShortHistory: [], 11 | customGui: {}, 12 | customScripts: [], 13 | defaultView: ViewMode.ALL, 14 | diffMode: DiffModes.THREE_UP.id, 15 | enabled: false, 16 | errorPatterns: [], 17 | lazyLoadOffset: null, 18 | metaInfoBaseUrls: {}, 19 | path: '', 20 | plugins: [], 21 | pluginsEnabled: false, 22 | saveErrorDetails: false, 23 | saveFormat: SaveFormat.SQLITE, 24 | yandexMetrika: { 25 | counterNumber: 99267510 26 | }, 27 | staticImageAccepter: { 28 | enabled: false, 29 | repositoryUrl: '', 30 | pullRequestUrl: '', 31 | serviceUrl: '', 32 | meta: {}, 33 | axiosRequestOptions: {} 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /lib/constants/errors.ts: -------------------------------------------------------------------------------- 1 | export const ERROR_TITLE_TEXT_LENGTH = 200; 2 | 3 | export const NEW_ISSUE_LINK = 'https://github.com/gemini-testing/html-reporter/issues/new'; 4 | 5 | export const IMAGE_COMPARISON_FAILED_MESSAGE = 'image comparison failed'; 6 | -------------------------------------------------------------------------------- /lib/constants/expand-modes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const EXPAND_ALL = 'all'; 4 | export const COLLAPSE_ALL = 'none'; 5 | export const EXPAND_ERRORS = 'errors'; 6 | export const EXPAND_RETRIES = 'retries'; 7 | -------------------------------------------------------------------------------- /lib/constants/extension-points.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const RESULT = 'result'; 4 | export const RESULT_META = 'result_meta'; 5 | export const MENU_BAR = 'menu-bar'; 6 | export const ROOT = 'root'; 7 | -------------------------------------------------------------------------------- /lib/constants/features.ts: -------------------------------------------------------------------------------- 1 | export interface Feature { 2 | name: string; 3 | } 4 | 5 | export const RunTestsFeature = { 6 | name: 'run-tests' 7 | } as const satisfies Feature; 8 | 9 | export const EditScreensFeature = { 10 | name: 'edit-screens' 11 | } as const satisfies Feature; 12 | 13 | export const TimeTravelFeature = { 14 | name: 'time-travel' 15 | } as const satisfies Feature; 16 | -------------------------------------------------------------------------------- /lib/constants/group-tests.js: -------------------------------------------------------------------------------- 1 | exports.SECTIONS = { 2 | RESULT: 'result', 3 | META: 'meta' 4 | }; 5 | exports.ERROR_KEY = 'error'; 6 | exports.RESULT_KEYS = [exports.ERROR_KEY]; 7 | exports.KEY_DELIMITER = '.'; 8 | -------------------------------------------------------------------------------- /lib/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browser'; 2 | export * from './database'; 3 | export * from './defaults'; 4 | export * from './diff-modes'; 5 | export * from './errors'; 6 | export * from './features'; 7 | export * from './group-tests'; 8 | export * from './paths'; 9 | export * from './tests'; 10 | export * from './plugin-events'; 11 | export * from './save-formats'; 12 | export * from './test-statuses'; 13 | export * from './tool-names'; 14 | export * from './view-modes'; 15 | -------------------------------------------------------------------------------- /lib/constants/local-storage.ts: -------------------------------------------------------------------------------- 1 | export enum LocalStorageKey { 2 | UIMode = 'ui-mode', 3 | TimeTravelUseRecommendedSettings = 'time-travel-use-recommended-settings' 4 | } 5 | 6 | export const TIME_TRAVEL_PLAYER_VISIBILITY_KEY = 'time-travel-player-visibility'; 7 | 8 | export enum UiMode { 9 | Old = 'old', 10 | New = 'new', 11 | } 12 | -------------------------------------------------------------------------------- /lib/constants/paths.ts: -------------------------------------------------------------------------------- 1 | export const IMAGES_PATH = 'images'; 2 | export const SNAPSHOTS_PATH = 'snapshots'; 3 | export const ERROR_DETAILS_PATH = 'error-details'; 4 | -------------------------------------------------------------------------------- /lib/constants/performance-marks.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | JS_EXEC: 'js exec', 3 | DBS_LOADED: 'dbs loaded', 4 | DBS_MERGED: 'dbs merged', 5 | PLUGINS_LOADED: 'plugins loaded', 6 | DB_EXTRACTED_ROWS: 'db extracted rows', 7 | FULLY_LOADED: 'fully loaded' 8 | } as const; 9 | -------------------------------------------------------------------------------- /lib/constants/plugin-events.ts: -------------------------------------------------------------------------------- 1 | export enum PluginEvents { 2 | DATABASE_CREATED = 'databaseCreated', 3 | TEST_SCREENSHOTS_SAVED = 'testScreenshotsSaved', 4 | REPORT_SAVED = 'reportSaved', 5 | IMAGES_SAVER_UPDATED = 'imagesSaverUpdated' 6 | } 7 | -------------------------------------------------------------------------------- /lib/constants/save-formats.ts: -------------------------------------------------------------------------------- 1 | export enum SaveFormat { 2 | SQLITE = 'sqlite' 3 | } 4 | -------------------------------------------------------------------------------- /lib/constants/tests.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TITLE_DELIMITER = ' '; 2 | export const TESTPLANE_TITLE_DELIMITER = DEFAULT_TITLE_DELIMITER; 3 | export const PWT_TITLE_DELIMITER = ' › '; 4 | 5 | export const UNKNOWN_ATTEMPT = -1; 6 | 7 | export const UNKNOWN_SESSION_ID = 'unknown session id'; 8 | -------------------------------------------------------------------------------- /lib/constants/tool-names.ts: -------------------------------------------------------------------------------- 1 | export enum ToolName { 2 | Testplane = 'testplane', 3 | Playwright = 'playwright', 4 | Jest = 'jest' 5 | } 6 | -------------------------------------------------------------------------------- /lib/constants/view-modes.ts: -------------------------------------------------------------------------------- 1 | export enum ViewMode { 2 | ALL = 'all', 3 | PASSED = 'passed', 4 | FAILED = 'failed', 5 | RETRIED = 'retried', 6 | SKIPPED = 'skipped', 7 | STAGED = 'staged', 8 | COMMITED = 'commited', 9 | } 10 | -------------------------------------------------------------------------------- /lib/gui/api/facade.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter2 from 'eventemitter2'; 2 | import {GuiEvents} from '../constants'; 3 | 4 | export class ApiFacade extends EventEmitter2 { 5 | events: GuiEvents; 6 | 7 | static create(this: new () => T): T { 8 | return new this(); 9 | } 10 | 11 | constructor() { 12 | super(); 13 | 14 | this.events = GuiEvents; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/gui/api/index.ts: -------------------------------------------------------------------------------- 1 | import {ApiFacade} from './facade'; 2 | import {Express} from 'express'; 3 | 4 | export interface ServerReadyData { 5 | url: string; 6 | } 7 | 8 | export class GuiApi { 9 | private _gui: ApiFacade; 10 | 11 | static create(this: new () => T): T { 12 | return new this(); 13 | } 14 | 15 | constructor() { 16 | this._gui = ApiFacade.create(); 17 | } 18 | 19 | async initServer(server: Express): Promise { 20 | await this._gui.emitAsync(this._gui.events.SERVER_INIT, server); 21 | } 22 | 23 | async serverReady(data: ServerReadyData): Promise { 24 | await this._gui.emitAsync(this._gui.events.SERVER_READY, data); 25 | } 26 | 27 | get gui(): ApiFacade { 28 | return this._gui; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/gui/constants/client-events.ts: -------------------------------------------------------------------------------- 1 | import {ValueOf} from 'type-fest'; 2 | 3 | export const ClientEvents = { 4 | BEGIN_SUITE: 'beginSuite', 5 | BEGIN_STATE: 'beginState', 6 | 7 | TEST_RESULT: 'testResult', 8 | 9 | RETRY: 'retry', 10 | ERROR: 'err', 11 | 12 | END: 'end', 13 | 14 | CONNECTED: 'connected', 15 | 16 | DOM_SNAPSHOTS: 'DOM_SNAPSHOTS' 17 | } as const; 18 | 19 | export type ClientEvents = typeof ClientEvents; 20 | 21 | export type ClientEvent = ValueOf; 22 | -------------------------------------------------------------------------------- /lib/gui/constants/custom-gui-control-types.ts: -------------------------------------------------------------------------------- 1 | export const CONTROL_TYPE_BUTTON = 'button'; 2 | 3 | export const CONTROL_TYPE_RADIOBUTTON = 'radiobutton'; 4 | -------------------------------------------------------------------------------- /lib/gui/constants/gui-events.ts: -------------------------------------------------------------------------------- 1 | import {ValueOf} from 'type-fest'; 2 | 3 | export const GuiEvents = { 4 | SERVER_INIT: 'serverInit', 5 | SERVER_READY: 'serverReady' 6 | } as const; 7 | 8 | export type GuiEvents = typeof GuiEvents; 9 | 10 | export type GuiEvent = ValueOf; 11 | -------------------------------------------------------------------------------- /lib/gui/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client-events'; 2 | export * from './custom-gui-control-types'; 3 | export * from './gui-events'; 4 | export * from './server'; 5 | -------------------------------------------------------------------------------- /lib/gui/constants/server.ts: -------------------------------------------------------------------------------- 1 | export const MAX_REQUEST_SIZE = '100mb'; 2 | 3 | export const KEEP_ALIVE_TIMEOUT = 120 * 1000; 4 | 5 | export const HEADERS_TIMEOUT = 125 * 1000; 6 | -------------------------------------------------------------------------------- /lib/gui/event-source.ts: -------------------------------------------------------------------------------- 1 | import {Response} from 'express'; 2 | import stringify from 'json-stringify-safe'; 3 | import {ClientEvents} from './constants'; 4 | 5 | export class EventSource { 6 | private _connections: Response[]; 7 | constructor() { 8 | this._connections = []; 9 | } 10 | 11 | private _write(connection: Response, event: string, data?: unknown): void { 12 | connection.write('event: ' + event + '\n'); 13 | connection.write('data: ' + stringify(data) + '\n'); 14 | connection.write('\n\n'); 15 | } 16 | 17 | addConnection(connection: Response): void { 18 | this._connections.push(connection); 19 | 20 | this._write(connection, ClientEvents.CONNECTED, 1); 21 | } 22 | 23 | emit(event: string, data?: unknown): void { 24 | this._connections.forEach((connection) => { 25 | this._write(connection, event, data); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/image-cache.ts: -------------------------------------------------------------------------------- 1 | export const cacheAllImages = new Map(); 2 | export const cacheDiffImages = new Map(); 3 | export const cacheExpectedPaths = new Map(); 4 | -------------------------------------------------------------------------------- /lib/local-image-file-saver.ts: -------------------------------------------------------------------------------- 1 | import {copyFileAsync} from './server-utils'; 2 | import type {ImageFileSaver} from './types'; 3 | 4 | export const LocalImageFileSaver: ImageFileSaver = { 5 | saveImg: async (srcCurrPath, {destPath, reportDir}) => { 6 | await copyFileAsync(srcCurrPath, destPath, {reportDir}); 7 | 8 | return destPath; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /lib/plugin-utils.ts: -------------------------------------------------------------------------------- 1 | import {TestplaneSuite} from './types'; 2 | 3 | export const getSuitePath = (suite?: TestplaneSuite | null): string[] => { 4 | if (!suite) { 5 | return []; 6 | } 7 | 8 | return (suite as TestplaneSuite).root ? 9 | [] : 10 | ([] as string[]).concat(getSuitePath(suite.parent as TestplaneSuite)).concat(suite.title); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/static/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: {browser: true}, 3 | plugins: [ 4 | 'react' 5 | ], 6 | rules: { 7 | 'react/jsx-uses-react': 'error', 8 | 'react/jsx-uses-vars': 'error', 9 | 'react/jsx-no-undef': ['error', {allowGlobals: true}], 10 | 'object-curly-spacing': 'off', 11 | '@typescript-eslint/object-curly-spacing': 'error' 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/static/components/controls/browser-list/index.styl: -------------------------------------------------------------------------------- 1 | .g-popup 2 | z-index 9999 3 | .browserlist__filter 4 | width 100% 5 | display inline-flex 6 | justify-content center 7 | padding 4px 4px 8 | 9 | .browserlist__row 10 | width 100% 11 | display flex 12 | align-items center 13 | gap 4px 14 | 15 | .browserlist__row_content 16 | flex 1 1 auto 17 | 18 | .action-button 19 | width 60px 20 | opacity 0 21 | 22 | .g-select-list__option:hover 23 | .browserlist__row 24 | .action-button 25 | opacity 1 26 | 27 | .browserlist__popup 28 | .g-select-list__option 29 | gap 8px 30 | flex-direction: row-reverse 31 | 32 | .browser-name 33 | display inline-flex 34 | gap 4px 35 | 36 | .browserlist 37 | width 250px -------------------------------------------------------------------------------- /lib/static/components/controls/report-controls.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import React, {Component} from 'react'; 3 | import CommonControls from './common-controls'; 4 | import CommonFilters from './common-filters'; 5 | 6 | import './controls.less'; 7 | 8 | class ReportControls extends Component { 9 | render() { 10 | return ( 11 |
12 |
13 | 14 |
15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default ReportControls; 22 | -------------------------------------------------------------------------------- /lib/static/components/controls/run-button/index.styl: -------------------------------------------------------------------------------- 1 | .run-button { 2 | display: inline-flex; 3 | .run-button__dropdown { 4 | width: 150px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/static/components/controls/selects/index.styl: -------------------------------------------------------------------------------- 1 | .g-label.custom-label 2 | border-top-right-radius: 0 3 | border-bottom-right-radius: 0 4 | 5 | .group-by-dropdown 6 | width: 150px 7 | 8 | .select_type_control, .select_type_group 9 | display inline-flex -------------------------------------------------------------------------------- /lib/static/components/controls/selects/label.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Label} from '@gravity-ui/uikit'; 4 | import './index.styl'; 5 | import classNames from 'classnames'; 6 | 7 | const CustomLabel = ({className, ...otherProps}) => { 8 | return (); 9 | }; 10 | 11 | CustomLabel.propTypes = { 12 | className: PropTypes.string 13 | }; 14 | 15 | export default CustomLabel; 16 | -------------------------------------------------------------------------------- /lib/static/components/controls/show-checkboxes-input.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import useLocalStorage from '../../hooks/useLocalStorage'; 5 | import {Switch} from '@gravity-ui/uikit'; 6 | 7 | const ShowCheckboxesInput = () => { 8 | const [showCheckboxes, setShowCheckboxes] = useLocalStorage('showCheckboxes', false); 9 | 10 | const onChange = () => setShowCheckboxes(!showCheckboxes); 11 | 12 | return ( 13 | 14 | ); 15 | }; 16 | 17 | export default ShowCheckboxesInput; 18 | -------------------------------------------------------------------------------- /lib/static/components/error-boundary.js: -------------------------------------------------------------------------------- 1 | import {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class ErrorBoundary extends Component { 5 | static propTypes = { 6 | fallback: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), 7 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]) 8 | }; 9 | state = {hasError: false}; 10 | 11 | static getDerivedStateFromError() { 12 | return {hasError: true}; 13 | } 14 | 15 | componentDidCatch(error, errorInfo) { 16 | console.error('Something failed but catched by error boundary.'); 17 | console.error(error); 18 | console.error(errorInfo); 19 | } 20 | 21 | render() { 22 | if (this.state.hasError) { 23 | return this.props.fallback || null; 24 | } 25 | return this.props.children; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/static/components/group-tests/prop-types.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const groupedTestsType = PropTypes.shape({ 4 | result: PropTypes.shape({ 5 | byKey: PropTypes.object.isRequired, 6 | allKeys: PropTypes.array.isRequired 7 | }).isRequired, 8 | meta: PropTypes.shape({ 9 | byKey: PropTypes.object.isRequired, 10 | allKeys: PropTypes.array.isRequired 11 | }).isRequired 12 | }); 13 | -------------------------------------------------------------------------------- /lib/static/components/header/header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | padding-top: 15px; 3 | padding-bottom: 15px; 4 | border-bottom: 1px solid #ccc; 5 | } 6 | -------------------------------------------------------------------------------- /lib/static/components/header/index.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import {connect} from 'react-redux'; 5 | import PropTypes from 'prop-types'; 6 | import Summary from './summary'; 7 | 8 | import './header.css'; 9 | 10 | class Header extends Component { 11 | static propTypes = { 12 | date: PropTypes.string 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | } 23 | 24 | export default connect((state) => { 25 | return {date: state.date}; 26 | })(Header); 27 | -------------------------------------------------------------------------------- /lib/static/components/header/summary/dbBtn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Button} from '@gravity-ui/uikit'; 4 | import {ChevronDown} from '@gravity-ui/icons'; 5 | 6 | const ForwardedDbBtn = React.forwardRef(function DbBtn({fetchDbDetails}, ref) { 7 | const successFetchDbDetails = fetchDbDetails.filter(d => d.success); 8 | const isFailed = successFetchDbDetails.length !== fetchDbDetails.length; 9 | const value = `${successFetchDbDetails.length}/${fetchDbDetails.length}`; 10 | const content = `Databases loaded: ${value}`; 11 | 12 | return ( 13 | 21 | ); 22 | }); 23 | 24 | ForwardedDbBtn.propTypes = { 25 | fetchDbDetails: PropTypes.arrayOf(PropTypes.shape({ 26 | success: PropTypes.bool 27 | })).isRequired 28 | }; 29 | 30 | export default ForwardedDbBtn; 31 | -------------------------------------------------------------------------------- /lib/static/components/header/summary/item.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component, Fragment} from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import classNames from 'classnames'; 6 | 7 | export default class SummaryItem extends Component { 8 | static propTypes = { 9 | id: PropTypes.oneOf(['total', 'passed', 'failed', 'retries', 'skipped']).isRequired, 10 | label: PropTypes.string.isRequired, 11 | value: PropTypes.number 12 | }; 13 | 14 | render() { 15 | const {id, label, value} = this.props; 16 | const className = classNames( 17 | 'summary__key', 18 | `summary__key_${id}` 19 | ); 20 | 21 | return ( 22 | 23 |
{label}
24 |
{value}
25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/static/components/icons/arrow.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class Arrow extends Component { 5 | static propTypes = { 6 | width: PropTypes.number, 7 | height: PropTypes.number 8 | }; 9 | 10 | static defaultProps = { 11 | width: 12, 12 | height: 7 13 | }; 14 | 15 | render() { 16 | const {width, height} = this.props; 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/static/components/icons/arrows-move.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class ArrowsMove extends Component { 5 | static propTypes = { 6 | width: PropTypes.number, 7 | height: PropTypes.number 8 | }; 9 | 10 | static defaultProps = { 11 | width: 100, 12 | height: 100 13 | }; 14 | 15 | render() { 16 | const {width, height} = this.props; 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/static/components/icons/view-in-browser/index.styl: -------------------------------------------------------------------------------- 1 | .view-in-browser { 2 | height: 16px; 3 | margin-left: 4px; 4 | color: black; 5 | opacity: 0.6; 6 | 7 | &:visited { 8 | color: black; 9 | } 10 | 11 | &:active, 12 | &:hover { 13 | &.view-in-browser_active { 14 | opacity: 1; 15 | cursor: pointer; 16 | } 17 | 18 | &.view-in-browser_disabled { 19 | cursor: default; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/static/components/loading.jsx: -------------------------------------------------------------------------------- 1 | 'use-strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import {Dimmer, Loader} from 'semantic-ui-react'; 6 | 7 | export default class Loading extends Component { 8 | static propTypes = { 9 | active: PropTypes.bool, 10 | content: PropTypes.string, 11 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]) 12 | }; 13 | 14 | render() { 15 | const {props: {children, active, content = 'Loading...'}} = this; 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/static/components/main-tree.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import GroupTestsList from './group-tests/list'; 6 | import Suites from './suites'; 7 | 8 | class MainTree extends Component { 9 | static propTypes = { 10 | // from store 11 | keyToGroupTestsBy: PropTypes.string.isRequired 12 | }; 13 | 14 | render() { 15 | return this.props.keyToGroupTestsBy 16 | ? 17 | : ; 18 | } 19 | } 20 | 21 | export default connect( 22 | ({view: {keyToGroupTestsBy}}) => ({keyToGroupTestsBy}) 23 | )(MainTree); 24 | -------------------------------------------------------------------------------- /lib/static/components/measurement-context.js: -------------------------------------------------------------------------------- 1 | import {createContext} from 'react'; 2 | 3 | export const MeasurementContext = createContext({measure: () => {}}); 4 | -------------------------------------------------------------------------------- /lib/static/components/modals/index.js: -------------------------------------------------------------------------------- 1 | export const types = { 2 | FIND_SAME_DIFFS: 'FindSameDiffs', 3 | SCREENSHOT_ACCEPTER: 'ScreenshotAccepter', 4 | STATIC_ACCEPTER_CONFIRM: 'StaticAccepterConfirm' 5 | }; 6 | export {default as FindSameDiffs} from './find-same-diffs'; 7 | export {default as ScreenshotAccepter} from './screenshot-accepter'; 8 | export {default as StaticAccepterConfirm} from './static-accepter-confirm'; 9 | -------------------------------------------------------------------------------- /lib/static/components/modals/modal.css: -------------------------------------------------------------------------------- 1 | .modal-open { 2 | overflow-y: hidden; 3 | } 4 | -------------------------------------------------------------------------------- /lib/static/components/modals/modal.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Portal} from '@gravity-ui/uikit'; 3 | import PropTypes from 'prop-types'; 4 | import './modal.css'; 5 | 6 | class Modal extends Component { 7 | componentDidMount() { 8 | document.body.classList.add('modal-open'); 9 | } 10 | 11 | componentWillUnmount() { 12 | document.body.classList.remove('modal-open'); 13 | } 14 | 15 | render() { 16 | const {className, children} = this.props; 17 | 18 | return ( 19 | 20 |
21 | {children} 22 |
23 |
24 | ); 25 | } 26 | } 27 | 28 | Modal.propTypes = { 29 | className: PropTypes.string, 30 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]) 31 | }; 32 | 33 | export default Modal; 34 | -------------------------------------------------------------------------------- /lib/static/components/modals/screenshot-accepter/meta.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {MetaInfo as MetaInfoContent} from '@/static/new-ui/components/MetaInfo'; 5 | 6 | export default class ScreenshotAccepterMeta extends Component { 7 | static propTypes = { 8 | showMeta: PropTypes.bool.isRequired, 9 | image: PropTypes.shape({ 10 | parentId: PropTypes.string 11 | }) 12 | }; 13 | 14 | render() { 15 | const {showMeta, image} = this.props; 16 | 17 | if (!showMeta || !image) { 18 | return null; 19 | } 20 | 21 | return ( 22 | 23 |
24 | 28 |
29 |
30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/static/components/modals/static-accepter-confirm/style.css: -------------------------------------------------------------------------------- 1 | .static-accepter-confirm { 2 | z-index: 99999; 3 | width: 400px; 4 | position: fixed; 5 | left: calc(50% - 200px); 6 | top: 60px; 7 | padding: 15px; 8 | } 9 | 10 | .static-accepter-confirm__controls { 11 | display: flex; 12 | gap: 5px; 13 | margin-top: 10px; 14 | } 15 | 16 | .static-accepter-confirm__controls button { 17 | flex-grow: 1; 18 | } 19 | 20 | .static-accepter-confirm__controls .static-accepter-confirm__cancel { 21 | margin-left: auto; 22 | } 23 | -------------------------------------------------------------------------------- /lib/static/components/progress-bar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './index.styl'; 5 | 6 | const ProgressBar = ({done, total, dataTestId}) => { 7 | const percent = (done / total).toFixed(2) * 100; 8 | 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | ProgressBar.propTypes = { 17 | done: PropTypes.number.isRequired, 18 | total: PropTypes.number.isRequired, 19 | dataTestId: PropTypes.string 20 | }; 21 | 22 | export default ProgressBar; 23 | -------------------------------------------------------------------------------- /lib/static/components/progress-bar/index.styl: -------------------------------------------------------------------------------- 1 | .progress-bar { 2 | position: relative; 3 | display: flex; 4 | border: 1px solid #ccc; 5 | border-radius: 5px; 6 | 7 | &__container { 8 | position: absolute; 9 | height: 100%; 10 | z-index: -1; 11 | background-color: #fc0; 12 | transition: width .25s ease-in-out, 13 | margin-right .25s ease-in-out; 14 | } 15 | 16 | &::after { 17 | box-sizing: border-box; 18 | width: 100%; 19 | padding-left: 10px; 20 | padding-right: 10px; 21 | text-align: center; 22 | align-self: center; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/static/components/prop-types.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const ImageData = PropTypes.shape({ 4 | path: PropTypes.string.isRequired, 5 | size: PropTypes.shape({ 6 | height: PropTypes.number.isRequired, 7 | width: PropTypes.number.isRequired 8 | }).isRequired 9 | }); 10 | -------------------------------------------------------------------------------- /lib/static/components/section/body/description.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Markdown from 'react-markdown'; 3 | import PropTypes from 'prop-types'; 4 | import Details from '../../details'; 5 | 6 | export default class Description extends Component { 7 | static propTypes = { 8 | content: PropTypes.string.isRequired 9 | }; 10 | 11 | _renderDescription = () => { 12 | return {this.props.content}; 13 | }; 14 | 15 | render() { 16 | return
; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/static/components/section/body/history/index.styl: -------------------------------------------------------------------------------- 1 | .history { 2 | white-space: pre-wrap; 3 | word-wrap: break-word; 4 | 5 | .history-item { 6 | display: inline-flex; 7 | padding-right: 8px; 8 | padding-top: 1px; 9 | padding-bottom: 1px; 10 | 11 | gap: 4px; 12 | .history-item__name { 13 | user-select: text; 14 | } 15 | 16 | .history-item__time { 17 | opacity: 0.5; 18 | user-select: text; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/static/components/section/body/page-screenshot.tsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Details from '../../details'; 3 | import {ImageFile} from '../../../../types'; 4 | import {Screenshot} from '@/static/new-ui/components/Screenshot'; 5 | 6 | interface PageScreenshotProps { 7 | image: ImageFile; 8 | } 9 | 10 | export class PageScreenshot extends Component { 11 | render(): JSX.Element { 12 | return
} 15 | />; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/static/components/section/utils.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | 3 | export const sectionStatusResolver = ({status, shouldBeOpened, sectionRoot}) => { 4 | const baseClasses = ['section', {'section_collapsed': !shouldBeOpened}]; 5 | 6 | return classNames(baseClasses, { 7 | [`section_status_${status}`]: status, 8 | 'section_root': sectionRoot 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/static/components/state/error-details.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import Details from '../details'; 6 | 7 | export default class ErrorDetails extends Component { 8 | static propTypes = { 9 | errorDetails: PropTypes.object.isRequired 10 | }; 11 | 12 | render() { 13 | const {title, filePath} = this.props.errorDetails; 14 | const content = ; 15 | 16 | return
; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/static/components/state/state-fail/index.styl: -------------------------------------------------------------------------------- 1 | .diff-modes 2 | display: flex 3 | justify-content: center 4 | margin: 10px auto 5 | -------------------------------------------------------------------------------- /lib/static/components/state/state-success.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import {isUpdatedStatus} from '../../../common-utils'; 6 | import {Screenshot} from '@/static/new-ui/components/Screenshot'; 7 | 8 | export default class StateSuccess extends Component { 9 | static propTypes = { 10 | status: PropTypes.string.isRequired, 11 | expectedImg: PropTypes.object.isRequired 12 | }; 13 | 14 | render() { 15 | const {status, expectedImg} = this.props; 16 | 17 | return ( 18 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/static/components/sticky-header/gui.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import StickyHeaderTemplate from '.'; 4 | import ControlButtons from '../controls/gui-controls'; 5 | 6 | const StickyHeader = () => ( 7 | 8 | 9 | 10 | ); 11 | 12 | export default StickyHeader; 13 | -------------------------------------------------------------------------------- /lib/static/components/sticky-header/index.styl: -------------------------------------------------------------------------------- 1 | .sticky-header 2 | display contents 3 | 4 | .sticky-header__wrap 5 | position fixed 6 | width 100% 7 | height 40px 8 | z-index 98 9 | text-align center 10 | background-color white 11 | border-bottom 1px solid #ccc 12 | transition transform 250ms ease-out 500ms 13 | transform-origin top 14 | transform translateY(-100%) 15 | 16 | &::before 17 | transform translateY(50%) rotate(180deg) 18 | 19 | .sticky-header__content 20 | position sticky 21 | top 0 22 | width 100% 23 | z-index 99 24 | transform-origin top 25 | transition transform 250ms linear 250ms 26 | background-color white 27 | border-bottom 1px solid #ccc 28 | 29 | &.sticky-header_unwrapped .sticky-header__wrap 30 | transition-delay 0ms 31 | 32 | &.sticky-header_wrapped:not(:hover) 33 | .sticky-header__wrap 34 | transform translateY(0) 35 | 36 | .sticky-header__content 37 | transform translateY(-100%) scaleY(0) 38 | -------------------------------------------------------------------------------- /lib/static/components/sticky-header/report.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import StickyHeaderTemplate from '.'; 4 | import Header from '../header'; 5 | import ControlButtons from '../controls/report-controls'; 6 | 7 | const StickyHeader = () => ( 8 | 9 |
10 | 11 | 12 | ); 13 | 14 | export default StickyHeader; 15 | -------------------------------------------------------------------------------- /lib/static/constants/sort-tests.ts: -------------------------------------------------------------------------------- 1 | import {SortByExpression, SortType} from '@/static/new-ui/types/store'; 2 | 3 | export const SORT_BY_NAME: SortByExpression = {id: 'by-name', label: 'Name', type: SortType.ByName}; 4 | export const SORT_BY_FAILED_RETRIES: SortByExpression = {id: 'by-failed-runs', label: 'Failed runs count', type: SortType.ByFailedRuns}; 5 | export const SORT_BY_TESTS_COUNT: SortByExpression = {id: 'by-tests-count', label: 'Tests count', type: SortType.ByTestsCount}; 6 | export const SORT_BY_START_TIME: SortByExpression = {id: 'by-start-time', label: 'Start time', type: SortType.ByStartTime}; 7 | export const SORT_BY_DURATION: SortByExpression = {id: 'by-duration', label: 'Duration', type: SortType.ByDuration}; 8 | export const SORT_BY_RELEVANCE: SortByExpression = {id: 'by-relevance', label: 'Relevance', type: SortType.ByRelevance}; 9 | 10 | export const DEFAULT_AVAILABLE_EXPRESSIONS: SortByExpression[] = [ 11 | SORT_BY_NAME, 12 | SORT_BY_FAILED_RETRIES, 13 | SORT_BY_START_TIME, 14 | SORT_BY_DURATION 15 | ]; 16 | -------------------------------------------------------------------------------- /lib/static/containers/array/array.styl: -------------------------------------------------------------------------------- 1 | .array 2 | display inline-flex 3 | height 25px 4 | font-size 11px 5 | border 1px solid #ccc 6 | border-radius var(--button-border-radius) 7 | user-select none 8 | 9 | &:hover 10 | border-color var(--button-hover-color) 11 | 12 | .array__container 13 | display inherit 14 | flex-wrap wrap 15 | overflow hidden 16 | 17 | .array__placeholder 18 | margin-left 8px 19 | line-height 22px 20 | opacity .6 21 | 22 | .array__item 23 | flex-shrink 0 24 | background var(--button-action-color) 25 | border 1px solid #cebe7d 26 | border-radius 5px 27 | padding 4px 5px 28 | margin 1px 29 | line-height 11px 30 | 31 | &.array__item_hidden 32 | display none 33 | -------------------------------------------------------------------------------- /lib/static/gui.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {createRoot} from 'react-dom/client'; 3 | import {Provider} from 'react-redux'; 4 | import store from './modules/store'; 5 | import Gui from './components/gui'; 6 | import {ThemeProvider} from '@gravity-ui/uikit'; 7 | 8 | import '@gravity-ui/uikit/styles/fonts.css'; 9 | import '@gravity-ui/uikit/styles/styles.css'; 10 | import {AnalyticsProvider} from '@/static/new-ui/providers/analytics'; 11 | 12 | const rootEl = document.getElementById('app'); 13 | const root = createRoot(rootEl); 14 | 15 | root.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /lib/static/hooks/useElementSize.js: -------------------------------------------------------------------------------- 1 | import {useState, useCallback, useLayoutEffect} from 'react'; 2 | 3 | import useEventListener from './useEventListener'; 4 | 5 | export default function useElementSize({shouldListenResize = false} = {}) { 6 | const [ref, setRef] = useState(null); 7 | const [size, setSize] = useState({ 8 | width: 0, 9 | height: 0, 10 | left: 0, 11 | top: 0 12 | }); 13 | 14 | const handlePosition = useCallback(() => { 15 | setSize({ 16 | width: ref && ref.offsetWidth || 0, 17 | height: ref && ref.offsetHeight || 0, 18 | left: ref && ref.offsetLeft || 0, 19 | top: ref && ref.offsetTop || 0 20 | }); 21 | }, [ref && ref.offsetHeight, ref && ref.offsetWidth, ref && ref.offsetLeft, ref && ref.offsetTop]); 22 | 23 | shouldListenResize && useEventListener('resize', handlePosition); 24 | 25 | useLayoutEffect(() => { 26 | handlePosition(); 27 | }, [ref && ref.offsetHeight, ref && ref.offsetWidth, ref && ref.offsetLeft, ref && ref.offsetTop]); 28 | 29 | return [setRef, size]; 30 | } 31 | -------------------------------------------------------------------------------- /lib/static/hooks/useEventListener.js: -------------------------------------------------------------------------------- 1 | import {useRef, useEffect, useLayoutEffect} from 'react'; 2 | 3 | export default function useEventListener(eventName, handler, element, options) { 4 | const savedHandler = useRef(handler); 5 | 6 | useLayoutEffect(() => { 7 | savedHandler.current = handler; 8 | }, [handler]); 9 | 10 | useEffect(() => { 11 | const targetElement = element && element.current || window; 12 | 13 | if (!targetElement || !targetElement.addEventListener) { 14 | return; 15 | } 16 | 17 | const listener = event => savedHandler.current(event); 18 | 19 | targetElement.addEventListener(eventName, listener, options); 20 | 21 | return () => { 22 | targetElement.removeEventListener(eventName, listener, options); 23 | }; 24 | }, [eventName, element, options]); 25 | } 26 | -------------------------------------------------------------------------------- /lib/static/hooks/useWindowSize.js: -------------------------------------------------------------------------------- 1 | import {useState, useLayoutEffect, useCallback} from 'react'; 2 | 3 | import useEventListener from './useEventListener'; 4 | 5 | export default function useWindowSize() { 6 | const [windowSize, setWindowSize] = useState({ 7 | width: 0, 8 | height: 0 9 | }); 10 | 11 | const handleSize = useCallback(() => { 12 | setWindowSize({ 13 | width: window.innerWidth, 14 | height: window.innerHeight 15 | }); 16 | }, [setWindowSize]); 17 | 18 | useEventListener('resize', handleSize); 19 | 20 | useLayoutEffect(() => { 21 | handleSize(); 22 | }, []); 23 | 24 | return windowSize; 25 | } 26 | -------------------------------------------------------------------------------- /lib/static/icons/broken-snapshot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/static/icons/exclamation-triangle-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/static/icons/favicon-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/lib/static/icons/favicon-failure.png -------------------------------------------------------------------------------- /lib/static/icons/favicon-running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/lib/static/icons/favicon-running.png -------------------------------------------------------------------------------- /lib/static/icons/favicon-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/lib/static/icons/favicon-success.png -------------------------------------------------------------------------------- /lib/static/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/lib/static/icons/favicon.png -------------------------------------------------------------------------------- /lib/static/icons/testplane-mono.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/static/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {createRoot} from 'react-dom/client'; 3 | import {Provider} from 'react-redux'; 4 | import store from './modules/store'; 5 | import Report from './components/report'; 6 | import {ThemeProvider} from '@gravity-ui/uikit'; 7 | 8 | import '@gravity-ui/uikit/styles/fonts.css'; 9 | import '@gravity-ui/uikit/styles/styles.css'; 10 | import {AnalyticsProvider} from '@/static/new-ui/providers/analytics'; 11 | 12 | const rootEl = document.getElementById('app'); 13 | const root = createRoot(rootEl); 14 | 15 | root.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /lib/static/modules/actions/browsers.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@/static/modules/actions/types'; 2 | import actionNames from '@/static/modules/action-names'; 3 | import {BrowserFeature} from '@/constants/browser'; 4 | 5 | type SetBrowserFeaturesAction = Action; 7 | }>; 8 | export const setBrowserFeatures = (payload: SetBrowserFeaturesAction['payload']): SetBrowserFeaturesAction => ({ 9 | type: actionNames.SET_BROWSER_FEATURES, 10 | payload 11 | }); 12 | 13 | export type BrowsersAction = SetBrowserFeaturesAction; 14 | -------------------------------------------------------------------------------- /lib/static/modules/actions/features.ts: -------------------------------------------------------------------------------- 1 | import type {Action} from '@/static/modules/actions/types'; 2 | import actionNames from '@/static/modules/action-names'; 3 | import {Feature} from '@/constants'; 4 | 5 | export type SetAvailableFeaturesAction = Action; 8 | export const setAvailableFeatures = (payload: SetAvailableFeaturesAction['payload']): SetAvailableFeaturesAction => ({ 9 | type: actionNames.SET_AVAILABLE_FEATURES, 10 | payload 11 | }); 12 | 13 | export type FeaturesAction = 14 | | SetAvailableFeaturesAction; 15 | -------------------------------------------------------------------------------- /lib/static/modules/actions/group-tests.ts: -------------------------------------------------------------------------------- 1 | import actionNames from '@/static/modules/action-names'; 2 | import {Action} from '@/static/modules/actions/types'; 3 | 4 | /** This action is used in old UI only */ 5 | type GroupTestsByKeyAction = Action; 6 | export const groupTestsByKey = (payload: string | undefined): GroupTestsByKeyAction => ({type: actionNames.GROUP_TESTS_BY_KEY, payload}); 7 | 8 | type SetCurrentGroupByExpressionAction = Action; 11 | export const setCurrentGroupByExpression = (payload: SetCurrentGroupByExpressionAction['payload']): SetCurrentGroupByExpressionAction => 12 | ({type: actionNames.GROUP_TESTS_SET_CURRENT_EXPRESSION, payload}); 13 | 14 | export type GroupTestsAction = 15 | | SetCurrentGroupByExpressionAction 16 | | GroupTestsByKeyAction; 17 | -------------------------------------------------------------------------------- /lib/static/modules/actions/gui-server-connection.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@/static/modules/actions/types'; 2 | import actionNames from '@/static/modules/action-names'; 3 | 4 | type SetGuiServerConnectionStatusAction = Action; 7 | export const setGuiServerConnectionStatus = (payload: SetGuiServerConnectionStatusAction['payload']): SetGuiServerConnectionStatusAction => 8 | ({type: actionNames.SET_GUI_SERVER_CONNECTION_STATUS, payload}); 9 | 10 | export type GuiServerConnectionAction = SetGuiServerConnectionStatusAction; 11 | -------------------------------------------------------------------------------- /lib/static/modules/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './custom-gui'; 2 | export * from './filter-tests'; 3 | export * from './find-same-diffs'; 4 | export * from './group-tests'; 5 | export * from './gui-server-connection'; 6 | export * from './lifecycle'; 7 | export * from './loading'; 8 | export * from './modals'; 9 | export * from './notifications'; 10 | export * from './processing'; 11 | export * from './run-tests'; 12 | export * from './screenshots'; 13 | export * from './settings'; 14 | export * from './sort-tests'; 15 | export * from './static-accepter'; 16 | export * from './suites-page'; 17 | export * from './suites-tree-state'; 18 | export * from './visual-checks-page'; 19 | export * from './browsers'; 20 | -------------------------------------------------------------------------------- /lib/static/modules/actions/loading.ts: -------------------------------------------------------------------------------- 1 | import actionNames from '@/static/modules/action-names'; 2 | import type {Action} from '@/static/modules/actions/types'; 3 | 4 | export type ToggleLoadingAction = Action; 8 | export const toggleLoading = (payload: ToggleLoadingAction['payload']): ToggleLoadingAction => ({type: actionNames.TOGGLE_LOADING, payload}); 9 | 10 | export type LoadingAction = 11 | | ToggleLoadingAction; 12 | -------------------------------------------------------------------------------- /lib/static/modules/actions/modals.ts: -------------------------------------------------------------------------------- 1 | import type {Action} from '@/static/modules/actions/types'; 2 | import actionNames from '@/static/modules/action-names'; 3 | 4 | export type OpenModalAction = Action; 9 | export const openModal = (payload: OpenModalAction['payload']): OpenModalAction => ({type: actionNames.OPEN_MODAL, payload}); 10 | 11 | export type CloseModalAction = Action; 14 | export const closeModal = (payload: CloseModalAction['payload']): CloseModalAction => ({type: actionNames.CLOSE_MODAL, payload}); 15 | 16 | export type ModalsAction = 17 | | OpenModalAction 18 | | CloseModalAction; 19 | -------------------------------------------------------------------------------- /lib/static/modules/actions/notifications.ts: -------------------------------------------------------------------------------- 1 | import { 2 | notify, 3 | POSITIONS, 4 | Status as NotificationStatus, Notification 5 | } from 'reapop'; 6 | 7 | import {getHttpErrorMessage} from '@/static/modules/utils'; 8 | 9 | type UpsertNotificationAction = ReturnType; 10 | 11 | export const createNotification = (id: string, status: NotificationStatus, message: string, props: Partial = {}): UpsertNotificationAction => { 12 | const notificationProps: Partial = { 13 | position: POSITIONS.topCenter, 14 | dismissAfter: 5000, 15 | dismissible: true, 16 | showDismissButton: true, 17 | allowHTML: true, 18 | ...props 19 | }; 20 | 21 | return notify({id, status, message, ...notificationProps}); 22 | }; 23 | 24 | export const createNotificationError = (id: string, error: Error, props: Partial = {dismissAfter: 0}): UpsertNotificationAction => 25 | createNotification(id, 'error', getHttpErrorMessage(error), props); 26 | 27 | export {dismissNotification} from 'reapop'; 28 | -------------------------------------------------------------------------------- /lib/static/modules/actions/processing.ts: -------------------------------------------------------------------------------- 1 | import actionNames from '@/static/modules/action-names'; 2 | import {Action} from '@/static/modules/actions/types'; 3 | 4 | export type ProcessBeginAction = Action; 5 | export const processBegin = (): ProcessBeginAction => ({type: actionNames.PROCESS_BEGIN}); 6 | 7 | export type ProcessEndAction = Action; 8 | export const processEnd = (): ProcessEndAction => ({type: actionNames.PROCESS_END}); 9 | 10 | export type ProcessingAction = 11 | | ProcessBeginAction 12 | | ProcessEndAction; 13 | -------------------------------------------------------------------------------- /lib/static/modules/actions/settings.ts: -------------------------------------------------------------------------------- 1 | import {DiffModeId} from '@/constants'; 2 | import {Action} from '@/static/modules/actions/types'; 3 | import actionNames from '@/static/modules/action-names'; 4 | 5 | type UpdateBaseHostAction = Action; 8 | export const updateBaseHost = (host: string): UpdateBaseHostAction => ({type: actionNames.VIEW_UPDATE_BASE_HOST, payload: {host}}); 9 | 10 | type SetDiffModeAction = Action; 13 | export const setDiffMode = (payload: SetDiffModeAction['payload']): SetDiffModeAction => ({type: actionNames.SET_DIFF_MODE, payload}); 14 | 15 | export type SettingsAction = 16 | | UpdateBaseHostAction 17 | | SetDiffModeAction; 18 | -------------------------------------------------------------------------------- /lib/static/modules/actions/sort-tests.ts: -------------------------------------------------------------------------------- 1 | import actionNames from '@/static/modules/action-names'; 2 | import {Action} from '@/static/modules/actions/types'; 3 | import {SortDirection} from '@/static/new-ui/types/store'; 4 | 5 | type SetCurrentSortByExpressionAction = Action; 8 | export const setCurrentSortByExpression = (payload: SetCurrentSortByExpressionAction['payload']): SetCurrentSortByExpressionAction => 9 | ({type: actionNames.SORT_TESTS_SET_CURRENT_EXPRESSION, payload}); 10 | 11 | type SetSortByDirectionAction = Action; 14 | export const setSortByDirection = (payload: SetSortByDirectionAction['payload']): SetSortByDirectionAction => 15 | ({type: actionNames.SORT_TESTS_SET_DIRECTION, payload}); 16 | 17 | export type SortTestsAction = 18 | | SetCurrentSortByExpressionAction 19 | | SetSortByDirectionAction; 20 | -------------------------------------------------------------------------------- /lib/static/modules/actions/visual-checks-page.ts: -------------------------------------------------------------------------------- 1 | import actionNames from '@/static/modules/action-names'; 2 | import {Action} from '@/static/modules/actions/types'; 3 | 4 | export type VisualChecksPageSetCurrentNamedImageAction = Action; 7 | 8 | export const visualChecksPageSetCurrentNamedImage = (namedImageId: string): VisualChecksPageSetCurrentNamedImageAction => { 9 | return {type: actionNames.VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE, payload: {namedImageId}}; 10 | }; 11 | 12 | export type VisualChecksPageAction = 13 | | VisualChecksPageSetCurrentNamedImageAction; 14 | -------------------------------------------------------------------------------- /lib/static/modules/custom-queries.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {parseQuery, decodeBrowsers} from './query-params'; 4 | import {pick, has} from 'lodash'; 5 | import {ViewMode} from '../../constants/view-modes'; 6 | import {configDefaults} from '../../constants/defaults'; 7 | 8 | const allowedViewModes = new Set(Object.values(ViewMode)); 9 | 10 | export function getViewQuery(queryString) { 11 | const query = parseQuery(queryString, {browser: 'filteredBrowsers'}); 12 | 13 | query.filteredBrowsers = decodeBrowsers(query.filteredBrowsers); 14 | 15 | if (has(query, 'viewMode') && !allowedViewModes.has(query.viewMode)) { 16 | query.viewMode = configDefaults.defaultView; 17 | } 18 | 19 | return pick(query, [ 20 | 'filteredBrowsers', 21 | 'testNameFilter', 22 | 'strictMatchFilter', 23 | 'retryIndex', 24 | 'viewMode', 25 | 'expand' 26 | ]); 27 | } 28 | -------------------------------------------------------------------------------- /lib/static/modules/middlewares/local-storage.js: -------------------------------------------------------------------------------- 1 | import * as localStorageWrapper from '../local-storage-wrapper'; 2 | import actionNames from '../action-names'; 3 | 4 | export default store => next => action => { 5 | const result = next(action); 6 | 7 | if (shouldUpdateLocalStorage(action.type)) { 8 | const {view} = store.getState(); 9 | // do not save text inputs: 10 | // for example, a user opens a new report and sees no tests in it 11 | // as the filter is applied from the previous opening of another report 12 | localStorageWrapper.setItem('view', { 13 | expand: view.expand, 14 | viewMode: view.viewMode, 15 | diffMode: view.diffMode, 16 | strictMatchFilter: view.strictMatchFilter 17 | }); 18 | } 19 | 20 | return result; 21 | }; 22 | 23 | function shouldUpdateLocalStorage(actionType) { 24 | return /^VIEW_/.test(actionType) 25 | || [ 26 | actionNames.INIT_GUI_REPORT, 27 | actionNames.INIT_STATIC_REPORT, 28 | actionNames.CHANGE_VIEW_MODE 29 | ].includes(actionType); 30 | } 31 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/api-values.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | import {applyStateUpdate} from '../utils/state'; 3 | 4 | export default (state, action) => { 5 | switch (action.type) { 6 | case actionNames.INIT_GUI_REPORT: 7 | case actionNames.INIT_STATIC_REPORT: { 8 | const {apiValues} = action.payload; 9 | 10 | return applyStateUpdate(state, {apiValues}); 11 | } 12 | 13 | default: 14 | return state; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/auto-run.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case actionNames.INIT_GUI_REPORT: { 6 | const {autoRun = false} = action.payload; 7 | 8 | return {...state, autoRun}; 9 | } 10 | 11 | default: 12 | return state; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/bottom-progress-bar.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | import {applyStateUpdate} from '../utils/state'; 3 | 4 | export default ((state, action) => { 5 | switch (action.type) { 6 | case actionNames.UPDATE_BOTTOM_PROGRESS_BAR: { 7 | const {currentRootSuiteId} = action.payload; 8 | return applyStateUpdate(state, {progressBar: {currentRootSuiteId}}); 9 | } 10 | 11 | default: 12 | return state; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/close-ids.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case actionNames.CLOSE_SECTIONS: { 6 | return {...state, closeIds: action.payload}; 7 | } 8 | 9 | default: 10 | return state; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/date.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | import {dateToLocaleString} from '../utils'; 3 | 4 | // TODO: remove in next major (should use timestamp instead) 5 | export default (state, action) => { 6 | switch (action.type) { 7 | case actionNames.INIT_GUI_REPORT: 8 | case actionNames.INIT_STATIC_REPORT: { 9 | const {date} = action.payload; 10 | 11 | return {...state, date: dateToLocaleString(date)}; 12 | } 13 | 14 | default: 15 | return state; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/db.js: -------------------------------------------------------------------------------- 1 | import {closeDatabase} from '../../../db-utils/client'; 2 | import actionNames from '../action-names'; 3 | 4 | export default (state, action) => { 5 | switch (action.type) { 6 | case actionNames.INIT_STATIC_REPORT: 7 | case actionNames.INIT_GUI_REPORT: { 8 | const {db} = action.payload; 9 | 10 | return {...state, db}; 11 | } 12 | 13 | case actionNames.TESTS_END: { 14 | closeDatabase(state.db); // close previous connection in order to free memory 15 | const {db} = action.payload; 16 | 17 | return {...state, db}; 18 | } 19 | 20 | case actionNames.FIN_STATIC_REPORT: 21 | case actionNames.FIN_GUI_REPORT: { 22 | closeDatabase(state.db); 23 | 24 | return state; 25 | } 26 | 27 | default: 28 | return state; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/fetch-db-details.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case actionNames.INIT_STATIC_REPORT: { 6 | const {fetchDbDetails} = action.payload; 7 | return {...state, fetchDbDetails}; 8 | } 9 | 10 | default: 11 | return state; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/gui-server-connection.ts: -------------------------------------------------------------------------------- 1 | import {State} from '@/static/new-ui/types/store'; 2 | import {SomeAction} from '@/static/modules/actions/types'; 3 | import actionNames from '@/static/modules/action-names'; 4 | import {applyStateUpdate} from '@/static/modules/utils'; 5 | 6 | export default (state: State, action: SomeAction): State => { 7 | switch (action.type) { 8 | case actionNames.SET_GUI_SERVER_CONNECTION_STATUS: { 9 | return applyStateUpdate(state, { 10 | app: { 11 | guiServerConnection: { 12 | isConnected: action.payload.isConnected 13 | } 14 | } 15 | }); 16 | } 17 | default: 18 | return state; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/gui.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | import {applyStateUpdate} from '@/static/modules/utils/state'; 3 | import {EditScreensFeature, RunTestsFeature} from '@/constants'; 4 | 5 | export default (state, action) => { 6 | switch (action.type) { 7 | case actionNames.INIT_GUI_REPORT: { 8 | return applyStateUpdate(state, {gui: true, app: {availableFeatures: [RunTestsFeature, EditScreensFeature]}}); 9 | } 10 | 11 | case actionNames.INIT_STATIC_REPORT: { 12 | return {...state, gui: false}; 13 | } 14 | 15 | default: 16 | return state; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/is-initialized.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | import {applyStateUpdate} from '@/static/modules/utils'; 3 | 4 | export default (state, action) => { 5 | switch (action.type) { 6 | case actionNames.INIT_GUI_REPORT: 7 | case actionNames.INIT_STATIC_REPORT: 8 | return applyStateUpdate(state, {app: {isInitialized: true, loading: {isVisible: false}}}); 9 | 10 | default: 11 | return state; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/modals.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case actionNames.OPEN_MODAL: { 6 | return {...state, modals: state.modals.concat(action.payload)}; 7 | } 8 | 9 | case actionNames.CLOSE_MODAL: { 10 | return {...state, modals: state.modals.filter(({id}) => id !== action.payload.id)}; 11 | } 12 | 13 | default: 14 | return state; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/notifications.js: -------------------------------------------------------------------------------- 1 | import {reducer} from 'reapop'; 2 | 3 | export default (state, action) => { 4 | if (!action.type.startsWith('reapop/')) { 5 | return state; 6 | } 7 | 8 | return { 9 | ...state, 10 | notifications: reducer()([], action) 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/plugins.js: -------------------------------------------------------------------------------- 1 | import reduceReducers from 'reduce-reducers'; 2 | import * as plugins from '../plugins'; 3 | import actionNames from '../action-names'; 4 | 5 | const defaultPluginsReducers = state => state; 6 | let actualPluginsReducer = defaultPluginsReducers; 7 | 8 | export default function(state, action) { 9 | switch (action.type) { 10 | case actionNames.INIT_GUI_REPORT: 11 | case actionNames.INIT_STATIC_REPORT: { 12 | const pluginReducers = []; 13 | 14 | plugins.forEach(plugin => { 15 | if (Array.isArray(plugin.reducers)) { 16 | pluginReducers.push(reduceReducers(state, ...plugin.reducers)); 17 | } 18 | }); 19 | actualPluginsReducer = reduceReducers(state, ...pluginReducers); 20 | break; 21 | } 22 | } 23 | 24 | return actualPluginsReducer(state, action); 25 | } 26 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/processing.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case actionNames.RUN_ALL_TESTS: 6 | case actionNames.RUN_FAILED_TESTS: 7 | case actionNames.RETRY_SUITE: 8 | case actionNames.RETRY_TEST: 9 | case actionNames.PROCESS_BEGIN: { 10 | return {...state, processing: true}; 11 | } 12 | 13 | case actionNames.TESTS_END: 14 | case actionNames.PROCESS_END: { 15 | return {...state, processing: false}; 16 | } 17 | 18 | default: 19 | return state; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/running.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case actionNames.RUN_ALL_TESTS: 6 | case actionNames.RUN_FAILED_TESTS: 7 | case actionNames.RETRY_SUITE: 8 | case actionNames.RETRY_TEST: { 9 | return {...state, running: true}; 10 | } 11 | 12 | case actionNames.TESTS_END: { 13 | return {...state, running: false}; 14 | } 15 | 16 | default: 17 | return state; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/skips.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case actionNames.INIT_GUI_REPORT: 6 | case actionNames.INIT_STATIC_REPORT: { 7 | const {skips} = action.payload; 8 | 9 | return {...state, skips}; 10 | } 11 | 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/stats.ts: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | import {State} from '@/static/new-ui/types/store'; 3 | import {SomeAction} from '@/static/modules/actions/types'; 4 | import {applyStateUpdate} from '../utils'; 5 | 6 | export default (state: State, action: SomeAction): State => { 7 | switch (action.type) { 8 | case actionNames.INIT_STATIC_REPORT: { 9 | const {stats} = action.payload; 10 | const {perBrowser, ...restStats} = stats || {}; 11 | 12 | return applyStateUpdate(state, { 13 | stats: { 14 | all: restStats, 15 | perBrowser 16 | } 17 | }); 18 | } 19 | 20 | default: 21 | return state; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/stopping.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case actionNames.STOP_TESTS: { 6 | return {...state, stopping: true}; 7 | } 8 | 9 | case actionNames.TESTS_END: { 10 | return {...state, stopping: false}; 11 | } 12 | 13 | default: 14 | return state; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/timestamp.js: -------------------------------------------------------------------------------- 1 | import actionNames from '../action-names'; 2 | 3 | export default (state, action) => { 4 | switch (action.type) { 5 | case actionNames.INIT_GUI_REPORT: 6 | case actionNames.INIT_STATIC_REPORT: { 7 | const {timestamp} = action.payload; 8 | 9 | return {...state, timestamp}; 10 | } 11 | 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/static/modules/reducers/visual-checks-page.ts: -------------------------------------------------------------------------------- 1 | import {State} from '@/static/new-ui/types/store'; 2 | import actionNames from '@/static/modules/action-names'; 3 | import {applyStateUpdate} from '@/static/modules/utils/state'; 4 | import {VisualChecksPageAction} from '@/static/modules/actions'; 5 | 6 | export default (state: State, action: VisualChecksPageAction): State => { 7 | switch (action.type) { 8 | case actionNames.VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE: 9 | return applyStateUpdate(state, {app: {visualChecksPage: {currentNamedImageId: action.payload.namedImageId}}}) as State; 10 | default: 11 | return state; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/static/modules/selectors/grouped-tests.js: -------------------------------------------------------------------------------- 1 | import {createSelector} from 'reselect'; 2 | import {getKeyToGroupTestsBy} from './view'; 3 | import {parseKeyToGroupTestsBy} from '../utils'; 4 | 5 | export const getParsedKeyToGroupTestsBy = createSelector( 6 | getKeyToGroupTestsBy, 7 | (keyToGroupTestsBy) => keyToGroupTestsBy ? parseKeyToGroupTestsBy(keyToGroupTestsBy) : [] 8 | ); 9 | -------------------------------------------------------------------------------- /lib/static/modules/selectors/index.js: -------------------------------------------------------------------------------- 1 | export * as stats from './stats'; 2 | export * as tree from './tree'; 3 | export * as view from './view'; 4 | -------------------------------------------------------------------------------- /lib/static/modules/selectors/view.js: -------------------------------------------------------------------------------- 1 | export const getFilteredBrowsers = (state) => state.view.filteredBrowsers; 2 | export const getTestNameFilter = (state) => state.view.testNameFilter; 3 | export const getStrictMatchFilter = (state) => state.view.strictMatchFilter; 4 | export const getViewMode = (state) => state.view.viewMode; 5 | export const getKeyToGroupTestsBy = (state) => state.view.keyToGroupTestsBy; 6 | -------------------------------------------------------------------------------- /lib/static/modules/utils/performance.ts: -------------------------------------------------------------------------------- 1 | export function extractPerformanceMarks(): Record { 2 | const marks = performance?.getEntriesByType?.('mark') || []; 3 | 4 | return marks.reduce((acc, {name, startTime}) => { 5 | acc[name] = Math.round(startTime); 6 | 7 | return acc; 8 | }, {} as Record); 9 | } 10 | -------------------------------------------------------------------------------- /lib/static/modules/web-vitals.ts: -------------------------------------------------------------------------------- 1 | import {getCLS, getFID, getFCP, getLCP, getTTFB, ReportHandler} from 'web-vitals'; 2 | 3 | export const measurePerformance = (onReport: ReportHandler): void => { 4 | getCLS(onReport); 5 | getFID(onReport); 6 | getFCP(onReport); 7 | getLCP(onReport); 8 | getTTFB(onReport); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/static/new-ui/app/report.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode, useEffect} from 'react'; 2 | import {createRoot} from 'react-dom/client'; 3 | import {App} from './App'; 4 | import store from '../../modules/store'; 5 | import {thunkInitStaticReport, finStaticReport} from '../../modules/actions'; 6 | 7 | const rootEl = document.getElementById('app') as HTMLDivElement; 8 | const root = createRoot(rootEl); 9 | 10 | function Report(): ReactNode { 11 | useEffect(() => { 12 | store.dispatch(thunkInitStaticReport({isNewUi: true})); 13 | 14 | return () => { 15 | store.dispatch(finStaticReport()); 16 | }; 17 | }, []); 18 | 19 | return ; 20 | } 21 | 22 | root.render(); 23 | -------------------------------------------------------------------------------- /lib/static/new-ui/app/selectors.ts: -------------------------------------------------------------------------------- 1 | import {State} from '@/static/new-ui/types/store'; 2 | 3 | export const getTotalLoadingProgress = (state: State): number => { 4 | const progressValues = Object.values(state.app.loading.progress); 5 | 6 | if (progressValues.length === 0) { 7 | return 1; 8 | } 9 | 10 | const totalProgress = progressValues.reduce((acc, currentProgress) => { 11 | return acc + currentProgress; 12 | }, 0); 13 | 14 | return totalProgress / progressValues.length; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/AdaptiveSelect/index.module.css: -------------------------------------------------------------------------------- 1 | .select-popup { 2 | --g-color-text-info: #000; 3 | 4 | font-size: var(--g-text-body-1-font-size); 5 | } 6 | 7 | .select :global(.g-select-control__label) { 8 | margin-inline-end: 0; 9 | } 10 | 11 | .selected-option { 12 | margin-inline-start: 4px; 13 | } 14 | 15 | .label-icons-container { 16 | position: relative; 17 | padding-right: 2px; 18 | } 19 | 20 | .label-dot { 21 | display: none; 22 | position: absolute; 23 | right: 0; 24 | top: 0; 25 | } 26 | 27 | @container (max-width: 450px) { 28 | .select :global(.g-select-control__option-text) { 29 | display: none; 30 | } 31 | 32 | .label-dot { 33 | display: block; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/AsidePanel/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #fff; 3 | padding: 20px; 4 | width: 440px; 5 | height: 100%; 6 | overflow-x: scroll; 7 | 8 | --g-text-body-font-weight: 450; 9 | --g-text-body-short-font-size: 15px; 10 | } 11 | 12 | .divider { 13 | margin: 12px 0 20px 0; 14 | } 15 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/AsidePanel/index.tsx: -------------------------------------------------------------------------------- 1 | import {Divider} from '@gravity-ui/uikit'; 2 | import classNames from 'classnames'; 3 | import React, {ReactNode} from 'react'; 4 | 5 | import styles from './index.module.css'; 6 | 7 | interface AsidePanelProps { 8 | title: string; 9 | children?: ReactNode; 10 | className?: string; 11 | } 12 | 13 | export function AsidePanel(props: AsidePanelProps): ReactNode { 14 | return
15 |

{props.title}

16 | 17 | {props.children} 18 |
; 19 | } 20 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/AssertViewResult/index.module.css: -------------------------------------------------------------------------------- 1 | .screenshot { 2 | padding-right: 1px; 3 | } 4 | 5 | .screenshot-container { 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/AssertViewStatus/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | gap: 4px; 4 | align-items: center; 5 | 6 | color: var(--g-color-private-cool-grey-700-solid); 7 | font-size: 15px; 8 | font-weight: 450; 9 | } 10 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/AssertViewStatus/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import {ImageEntity} from '@/static/new-ui/types/store'; 3 | import styles from './index.module.css'; 4 | import {getAssertViewStatusIcon, getAssertViewStatusMessage} from '@/static/new-ui/utils/assert-view-status'; 5 | 6 | interface AssertViewStatusProps { 7 | image: ImageEntity | null; 8 | } 9 | 10 | export function AssertViewStatus({image}: AssertViewStatusProps): ReactNode { 11 | return
{getAssertViewStatusIcon(image)}{getAssertViewStatusMessage(image)}
; 12 | } 13 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Card/AnimatedAppearCard.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | 3 | import cardStyles from './index.module.css'; 4 | import styles from './AnimatedAppearCard.module.css'; 5 | import classNames from 'classnames'; 6 | import {useSelector} from 'react-redux'; 7 | 8 | export function AnimatedAppearCard(): ReactNode { 9 | const isInitialized = useSelector(state => state.app.isInitialized); 10 | 11 | return
12 |
13 |
; 14 | } 15 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Card/EmptyReportCard.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 10px; 3 | height: 100%; 4 | } 5 | 6 | .hint-card { 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | .empty-report-icon { 13 | width: 40px; 14 | } 15 | 16 | .card-title { 17 | color: #000; 18 | margin-top: 16px; 19 | } 20 | 21 | .hints-container { 22 | max-width: 450px; 23 | margin-top: 8px; 24 | } 25 | 26 | .hint { 27 | margin-top: 12px; 28 | display: flex; 29 | } 30 | 31 | .hint-item-icon { 32 | flex-shrink: 0; 33 | margin-right: 8px; 34 | color: var(--g-color-base-brand); 35 | } 36 | 37 | .hint-item-text { 38 | line-height: 1.4; 39 | } 40 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Card/KeepDraggingToHideCard.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, {ReactNode} from 'react'; 3 | 4 | import cardStyles from './index.module.css'; 5 | import styles from './KeepDraggingToHideCard.module.css'; 6 | 7 | export function KeepDraggingToHideCard(): ReactNode { 8 | return
9 |
10 | Keep dragging to hide 11 |
; 12 | } 13 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Card/TextHintCard.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: #fff; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | composes: text-hint from global; 7 | } 8 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Card/TextHintCard.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, {ReactNode} from 'react'; 3 | 4 | import {Card, CardProps} from '.'; 5 | import styles from './TextHintCard.module.css'; 6 | 7 | interface TextHintCardProps extends CardProps { 8 | children: ReactNode; 9 | } 10 | 11 | export function TextHintCard(props: TextHintCardProps): ReactNode { 12 | return {props.children}; 13 | } 14 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Card/UiCard.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: #fff; 3 | display: flex; 4 | flex-direction: column; 5 | padding: 0 20px 20px 20px; 6 | position: relative; 7 | } 8 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Card/UiCard.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, {ReactNode} from 'react'; 3 | 4 | import {Card, CardProps} from '.'; 5 | import styles from './UiCard.module.css'; 6 | 7 | export function UiCard(props: CardProps): ReactNode { 8 | return {props.children}; 9 | } 10 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Card/index.module.css: -------------------------------------------------------------------------------- 1 | .common-card { 2 | border-radius: 10px; 3 | position: relative; 4 | } 5 | 6 | .wrapper { 7 | overflow: hidden; 8 | box-shadow: rgb(255, 255, 255) 0 0 0 0, rgba(9, 9, 11, 0.05) 0 0 0 1px, rgba(0, 0, 0, 0.05) 0 1px 2px 0; 9 | border-radius: 10px; 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import styles from './index.module.css'; 5 | 6 | export interface CardProps { 7 | className?: string; 8 | children?: React.ReactNode; 9 | style?: React.CSSProperties; 10 | qa?: string; 11 | } 12 | 13 | export function Card(props: CardProps): React.ReactNode { 14 | return
15 |
16 | {props.children} 17 |
18 |
; 19 | } 20 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/ChangedDot/index.module.css: -------------------------------------------------------------------------------- 1 | .dot { 2 | width: 4px; 3 | height: 4px; 4 | border-radius: 100vh; 5 | background-color: var(--g-color-private-red-600-solid); 6 | } 7 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/ChangedDot/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.module.css'; 3 | import classNames from 'classnames'; 4 | 5 | interface ChangedDotProps { 6 | className?: string; 7 | } 8 | 9 | export function ChangedDot({className}: ChangedDotProps): React.ReactElement { 10 | return
; 11 | } 12 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/CompactAttemptPicker/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | gap: 4px; 4 | } 5 | 6 | .attempt-select { 7 | font-size: 15px; 8 | } 9 | 10 | .attempt-number { 11 | font-weight: 450; 12 | } 13 | 14 | .attempt-option { 15 | display: flex; 16 | gap: 8px; 17 | } 18 | 19 | .attempt-select-popup { 20 | max-height: 40vh; 21 | } 22 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/CustomScripts/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode, useEffect, useRef} from 'react'; 2 | 3 | interface CustomScriptProps { 4 | scripts: (string | ((...args: never) => unknown))[]; 5 | } 6 | 7 | export function CustomScripts(props: CustomScriptProps): ReactNode { 8 | const {scripts} = props; 9 | 10 | if (scripts.length === 0) { 11 | return null; 12 | } 13 | 14 | const ref = useRef(null); 15 | 16 | useEffect(() => { 17 | scripts.forEach((script) => { 18 | const s = document.createElement('script'); 19 | s.type = 'text/javascript'; 20 | s.innerHTML = `(${script})();`; 21 | ref.current?.appendChild(s); 22 | }); 23 | }, [scripts]); 24 | 25 | return
; 26 | } 27 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/ListMode.module.css: -------------------------------------------------------------------------------- 1 | .list-mode { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/ListMode.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import {Screenshot} from '@/static/new-ui/components/Screenshot'; 3 | 4 | import styles from './ListMode.module.css'; 5 | import {ScreenshotDisplayData} from '@/static/new-ui/components/DiffViewer/types'; 6 | 7 | interface SideBySideToFitModeProps { 8 | actual: ScreenshotDisplayData; 9 | diff: ScreenshotDisplayData; 10 | expected: ScreenshotDisplayData; 11 | } 12 | 13 | export function ListMode(props: SideBySideToFitModeProps): ReactNode { 14 | return
15 |
16 | {props.expected.label} 17 | 18 |
19 |
20 | {props.actual.label} 21 | 22 |
23 |
24 | {props.diff.label} 25 | 26 |
27 |
; 28 | } 29 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/OnionSkinMode.module.css: -------------------------------------------------------------------------------- 1 | .onion-skin { 2 | cursor: pointer; 3 | } 4 | 5 | .image-wrapper--actual { 6 | position: absolute; 7 | } 8 | 9 | .image { 10 | max-width: none; 11 | } 12 | 13 | .slider-container { 14 | width: 100%; 15 | display: flex; 16 | justify-content: center; 17 | } 18 | 19 | .slider { 20 | cursor: pointer; 21 | margin-top: 10px; 22 | width: 250px; 23 | } 24 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/OnlyDiffMode.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import {Screenshot} from '@/static/new-ui/components/Screenshot'; 3 | import {ImageFile} from '@/types'; 4 | import {CoordBounds} from 'looks-same'; 5 | 6 | interface OnlyDiffModeProps { 7 | diff: ImageFile & {diffClusters?: CoordBounds[]}; 8 | } 9 | 10 | export function OnlyDiffMode(props: OnlyDiffModeProps): ReactNode { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/SideBySideMode.module.css: -------------------------------------------------------------------------------- 1 | .side-by-side-mode { 2 | display: flex; 3 | justify-content: space-between; 4 | column-gap: 4px; 5 | } 6 | 7 | .image-wrapper { 8 | flex: var(--natural-width) 0 0; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/SideBySideToFitMode.module.css: -------------------------------------------------------------------------------- 1 | .side-by-side-to-fit-mode { 2 | display: flex; 3 | justify-content: space-between; 4 | column-gap: 4px; 5 | } 6 | 7 | .image-wrapper { 8 | flex: var(--natural-width) 0 0; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .image { 14 | max-height: max(var(--desired-height), calc(var(--natural-height) * 1px / 2)); 15 | width: var(--img-width); 16 | height: var(--img-height); 17 | } 18 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/SwipeMode.module.css: -------------------------------------------------------------------------------- 1 | .divider { 2 | cursor: col-resize; 3 | background-color: #000; 4 | height: 100%; 5 | position: relative; 6 | width: 1px; 7 | z-index: 2; 8 | } 9 | 10 | .divider-icons { 11 | display: flex; 12 | gap: 10px; 13 | left: 50%; 14 | position: absolute; 15 | top: 50%; 16 | transform: translate(-50%, -50%) scale(1.2); 17 | transition: .3s opacity ease; 18 | } 19 | 20 | .is-dragging { 21 | cursor: col-resize; 22 | } 23 | 24 | .is-dragging .divider-icons { 25 | opacity: 0; 26 | } 27 | 28 | .image { 29 | max-width: none; 30 | pointer-events: none; 31 | width: auto; 32 | } 33 | 34 | .left-section { 35 | background-color: white; 36 | max-width: fit-content; 37 | overflow: hidden; 38 | z-index: 1; 39 | } 40 | 41 | .right-section { 42 | position: absolute; 43 | } 44 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/SwitchMode.module.css: -------------------------------------------------------------------------------- 1 | .switch-mode { 2 | cursor: pointer; 3 | } 4 | 5 | .screenshot-container { 6 | position: absolute; 7 | } 8 | 9 | .image { 10 | max-width: none; 11 | } 12 | 13 | .image--hidden { 14 | visibility: hidden; 15 | } 16 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/common.module.css: -------------------------------------------------------------------------------- 1 | .images-container { 2 | aspect-ratio: var(--max-natural-width) / var(--max-natural-height); 3 | box-shadow: 0 0 0 1px #ccc; 4 | display: flex; 5 | max-width: calc(var(--max-natural-width) * 1px); 6 | overflow: hidden; 7 | position: relative; 8 | user-select: none; 9 | width: 100%; 10 | } 11 | 12 | .screenshot-container { 13 | height: calc(var(--natural-height) / var(--max-natural-height) * 100%); 14 | } 15 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/index.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/lib/static/new-ui/components/DiffViewer/index.module.css -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/types.ts: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | import {CoordBounds} from 'looks-same'; 3 | 4 | import {ImageFile} from '@/types'; 5 | 6 | export interface ScreenshotDisplayData extends ImageFile { 7 | label?: ReactNode; 8 | diffClusters?: CoordBounds[]; 9 | } 10 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/DiffViewer/utils.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ImageSize} from '@/types'; 3 | 4 | export const getImageSizeCssVars = (size: ImageSize): React.CSSProperties => ({ 5 | '--natural-width': size.width, 6 | '--natural-height': size.height, 7 | '--img-width': size.width > size.height ? '100%' : 'auto', 8 | '--img-height': size.width > size.height ? 'auto' : '100%' 9 | } as React.CSSProperties); 10 | 11 | export const getDisplayedDiffPercentValue = (diffRatio: number): string => { 12 | const percent = diffRatio * 100; 13 | const percentRounded = Math.ceil(percent * 100) / 100; 14 | const percentThreshold = 0.01; 15 | 16 | if (percent < percentThreshold) { 17 | return `< ${percentThreshold}`; 18 | } 19 | 20 | return String(percentRounded); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/ErrorInfo/index.module.css: -------------------------------------------------------------------------------- 1 | .code { 2 | white-space: pre; 3 | font-family: monospace; 4 | overflow-x: scroll; 5 | overflow-y: hidden; 6 | background: #101827; 7 | border-radius: 10px; 8 | padding: 12px; 9 | } 10 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/ErrorInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import ansiHtml from 'ansi-html-community'; 2 | import classNames from 'classnames'; 3 | import escapeHtml from 'escape-html'; 4 | import React, {ReactNode} from 'react'; 5 | 6 | import styles from './index.module.css'; 7 | 8 | interface ErrorInfoProps { 9 | name: string; 10 | stack?: string; 11 | className?: string; 12 | style?: React.CSSProperties; 13 | } 14 | 15 | export function ErrorInfo(props: ErrorInfoProps): ReactNode { 16 | ansiHtml.setColors({ 17 | reset: ['eee', '00000000'] 18 | }); 19 | 20 | return
; 21 | } 22 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/IconButton/index.tsx: -------------------------------------------------------------------------------- 1 | import {Button, ButtonView, Tooltip} from '@gravity-ui/uikit'; 2 | import React, {KeyboardEventHandler, MouseEventHandler, ReactNode} from 'react'; 3 | 4 | interface IconButtonProps { 5 | icon: ReactNode; 6 | tooltip: string; 7 | onClick?: MouseEventHandler; 8 | onKeyDown?: KeyboardEventHandler; 9 | view?: ButtonView; 10 | disabled?: boolean; 11 | className?: string; 12 | selected?: boolean; 13 | } 14 | 15 | export const IconButton = React.forwardRef(function IconButtonInternal(props, ref) { 16 | return 17 | 28 | ; 29 | }); 30 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/ImageLabel/index.module.css: -------------------------------------------------------------------------------- 1 | .image-label, .image-label + div { 2 | margin-bottom: 8px; 3 | } 4 | 5 | .image-label-subtitle { 6 | color: var(--g-color-private-black-400); 7 | margin-left: 4px; 8 | } 9 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/ImageLabel/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import styles from './index.module.css'; 3 | 4 | interface ImageLabelProps { 5 | title: string; 6 | subtitle?: string; 7 | } 8 | 9 | export function ImageLabel({title, subtitle}: ImageLabelProps): ReactNode { 10 | return
11 | {title} 12 | {subtitle && {subtitle}} 13 |
; 14 | } 15 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/ImageWithMagnifier/index.module.css: -------------------------------------------------------------------------------- 1 | .magnifier { 2 | background-color: white; 3 | background-repeat: no-repeat; 4 | border: 1px solid lightgrey; 5 | border-radius: 5px; 6 | opacity: 1; 7 | pointer-events: none; 8 | position: fixed; 9 | z-index: 1000 10 | } 11 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/MainLayout/index.module.css: -------------------------------------------------------------------------------- 1 | .aside-header-bg-wrapper { 2 | width: 100%; 3 | } 4 | 5 | .aside-header-bg { 6 | background-color: var(--color-bg-dark); 7 | width: 100%; 8 | } 9 | 10 | :global(.gn-composite-bar-item):has(.footer-item:global(.disabled)) { 11 | pointer-events: none; 12 | opacity: .5; 13 | } 14 | 15 | .footer-item { 16 | color: var(--gn-aside-header-item-icon-color) !important; 17 | } 18 | 19 | .footer-item--active { 20 | color: var(--gn-aside-header-item-current-icon-color) !important; 21 | } 22 | 23 | :global(.gn-composite-bar-item__menu-divider) { 24 | background: linear-gradient(90deg, #9ca3af00, #9ca3af3d, #9ca3af00); 25 | height: 1px; 26 | border-top: none !important ; 27 | } 28 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/MetaInfo/index.module.css: -------------------------------------------------------------------------------- 1 | .meta-info { 2 | padding-right: 30px; 3 | } 4 | 5 | .meta-info-value { 6 | -webkit-line-clamp: 2; 7 | overflow: hidden; 8 | text-overflow: ellipsis; 9 | display: -webkit-box; 10 | -webkit-box-orient: vertical; 11 | line-height: 1.2; 12 | } 13 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/NamedSwitch/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | align-items: center; 3 | display: flex; 4 | padding: 8px 0; 5 | justify-content: space-between; 6 | } 7 | 8 | .text-container { 9 | padding-right: 6px; 10 | } 11 | 12 | .title { 13 | font-weight: 450; 14 | } 15 | 16 | .description { 17 | margin-top: 6px; 18 | color: var(--g-color-private-black-400); 19 | line-height: 1.3; 20 | font-size: 14px; 21 | } 22 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/NamedSwitch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import {Switch} from '@gravity-ui/uikit'; 3 | 4 | import styles from './index.module.css'; 5 | 6 | interface NamedSwitchProps { 7 | title: string; 8 | description?: string; 9 | checked: boolean; 10 | onUpdate?: (value: boolean) => void; 11 | } 12 | 13 | export function NamedSwitch(props: NamedSwitchProps): ReactNode { 14 | return
15 |
16 |
{props.title}
17 | {props.description &&
{props.description}
} 18 |
19 | 20 |
; 21 | } 22 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/PanelSection/index.module.css: -------------------------------------------------------------------------------- 1 | .description { 2 | color: var(--g-color-private-black-400); 3 | margin: 8px 0 16px 0; 4 | line-height: 1.3; 5 | } 6 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/PanelSection/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, {ReactNode} from 'react'; 3 | import styles from './index.module.css'; 4 | 5 | interface PanelSectionProps { 6 | title: string; 7 | description?: ReactNode; 8 | children?: ReactNode; 9 | } 10 | 11 | export function PanelSection(props: PanelSectionProps): ReactNode { 12 | return
13 |
{props.title}
14 | {props.description &&
{props.description}
} 15 | {props.children} 16 |
; 17 | } 18 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Screenshot/DiffCircle.module.css: -------------------------------------------------------------------------------- 1 | .diff-circle { 2 | position: fixed; 3 | border-radius: 50%; 4 | z-index: 9999999; 5 | opacity: 0.3; 6 | background-color: #FF00FF; 7 | } 8 | 9 | @keyframes :global(diff-bubbles) { 10 | 100% { 11 | transform: scale(var(--diff-bubbles-scale)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Screenshot/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | display: inline-flex; 4 | } 5 | 6 | .container--clickable { 7 | cursor: pointer; 8 | } 9 | 10 | .image { 11 | aspect-ratio: var(--natural-width) / var(--natural-height); 12 | max-width: calc(min(var(--natural-width) * 1px, 100%)); 13 | width: 100%; 14 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); 15 | } 16 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/Screenshot/utils.ts: -------------------------------------------------------------------------------- 1 | import {isUrl} from '@/common-utils'; 2 | 3 | // To prevent image caching. 4 | export function addTimestamp(imagePath: string): string { 5 | return `${imagePath}?t=${Date.now()}`; 6 | } 7 | 8 | // Since local filenames may contain special characters like %, they need to be encoded. 9 | export function encodePathSegments(imagePath: string): string { 10 | if (isUrl(imagePath)) { 11 | return imagePath; 12 | } 13 | 14 | return imagePath 15 | // we can't use path.sep here because on Windows browser returns '/' instead of '\\' 16 | .split(/[/\\]/) 17 | .map((item) => encodeURIComponent(item)) 18 | .join('/'); 19 | } 20 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/SettingsPanel/index.module.css: -------------------------------------------------------------------------------- 1 | .divider { 2 | margin: 20px 0; 3 | } 4 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/ToolbarOverlay/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: fixed; 3 | z-index: 999; 4 | background: #6c47ff; 5 | padding: 12px 12px 12px 6px; 6 | border-radius: 10px; 7 | color: rgba(255, 255, 255, .9); 8 | fill: rgba(255, 255, 255, .9); 9 | width: 700px; 10 | display: flex; 11 | box-shadow: 0 0 16px 0 #00000036; 12 | align-items: center; 13 | 14 | opacity: 0; 15 | scale: 0.95; 16 | 17 | visibility: hidden; 18 | transition: scale .15s ease, visibility 0.15s ease, opacity 0.15s ease; 19 | } 20 | 21 | .dragging { 22 | scale: 1.01 !important; 23 | } 24 | 25 | .visible { 26 | visibility: visible; 27 | opacity: 1; 28 | scale: 1; 29 | } 30 | 31 | .icon-container { 32 | margin-right: 6px; 33 | cursor: grab; 34 | } 35 | 36 | .dragging .icon-container { 37 | cursor: grabbing; 38 | } 39 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/TreeViewItemIcon/index.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | align-items: center; 3 | display: flex; 4 | height: 1lh; 5 | } 6 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/TreeViewItemIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | 3 | import styles from './index.module.css'; 4 | 5 | interface TreeViewItemIconProps { 6 | children: ReactNode; 7 | } 8 | 9 | export function TreeViewItemIcon(props: TreeViewItemIconProps): ReactNode { 10 | return
{props.children}
; 11 | } 12 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/UiModeHintNotification/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | bottom: 6px; 3 | gap: 8px; 4 | left: 62px; 5 | } 6 | 7 | @keyframes arrow-shake { 8 | 0% { transform: translateX(-3px); } 9 | 50% { transform: translateX(3px); } 10 | 100% { transform: translateX(-3px); } 11 | } 12 | 13 | .arrow { 14 | animation: arrow-shake 6s infinite ease; 15 | } 16 | 17 | .hint-title { 18 | font-weight: 500; 19 | } 20 | 21 | .close-button { 22 | cursor: pointer; 23 | transition: opacity .4s ease; 24 | margin-left: auto; 25 | } 26 | 27 | .close-button:hover { 28 | opacity: 0.7; 29 | } 30 | -------------------------------------------------------------------------------- /lib/static/new-ui/components/UiModeHintNotification/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import {ArrowLeft, Xmark} from '@gravity-ui/icons'; 3 | 4 | import styles from './index.module.css'; 5 | import {ToolbarOverlay} from '@/static/new-ui/components/ToolbarOverlay'; 6 | 7 | interface HintNotificationProps { 8 | isVisible: boolean | null; 9 | onClose?: () => unknown; 10 | } 11 | 12 | export function UiModeHintNotification(props: HintNotificationProps): ReactNode { 13 | return 14 | 15 |
Hint
16 | 17 | 18 | 19 |
You can always switch back to the old UI in Settings
20 | props.onClose?.()}/> 21 |
; 22 | } 23 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/error-handling/components/ErrorHandling/context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ErrorContext} from './interfaces'; 3 | 4 | const Context = React.createContext(null); 5 | 6 | export const ErrorContextProvider = Context.Provider; 7 | 8 | export const useErrorContext = (): ErrorContext => { 9 | const ctx = React.useContext(Context); 10 | 11 | if (ctx === null) { 12 | throw new Error('useErrorContext must be used within ErrorContextProvider'); 13 | } 14 | 15 | return ctx; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/error-handling/components/ErrorHandling/index.tsx: -------------------------------------------------------------------------------- 1 | import {Boundary} from './Boundary'; 2 | import {FallbackAppCrash, FallbackCardCrash, FallbackDataCorruption} from './fallbacks'; 3 | 4 | export const ErrorHandler = { 5 | Boundary, 6 | FallbackAppCrash, 7 | FallbackCardCrash, 8 | FallbackDataCorruption 9 | }; 10 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/error-handling/components/ErrorHandling/interfaces.ts: -------------------------------------------------------------------------------- 1 | import {ReactNode, DependencyList, ErrorInfo} from 'react'; 2 | 3 | export interface BoundaryProps { 4 | /** Node to display when falling */ 5 | fallback?: ReactNode, 6 | /** Changing this primitive will update the component forcibly if it crashed with an error. */ 7 | watchFor?: DependencyList; 8 | children?: ReactNode 9 | } 10 | 11 | export interface BoundaryStateAlive { 12 | hasError: false; 13 | error: null; 14 | errorInfo: null; 15 | } 16 | 17 | export interface BoundaryStateDead { 18 | hasError: true; 19 | error: Error; 20 | errorInfo: ErrorInfo; 21 | } 22 | 23 | export interface BoundaryStateInternal { 24 | watchFor?: DependencyList 25 | } 26 | 27 | export type BoundaryState = (BoundaryStateAlive | BoundaryStateDead) & BoundaryStateInternal; 28 | 29 | export interface ErrorContext { 30 | state: BoundaryStateDead; 31 | restore(): void; 32 | } 33 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/info/components/InfoPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import {Box} from '@gravity-ui/uikit'; 3 | 4 | export function InfoPage(): ReactNode { 5 | return 6 |

Info

7 |
; 8 | } 9 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/BrowsersSelect/BrowserIcon.module.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | opacity: .6; 3 | } 4 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/BrowsersSelect/index.module.css: -------------------------------------------------------------------------------- 1 | :global(.g-popup) { 2 | z-index: 9999; 3 | } 4 | 5 | .browserlist__filter { 6 | padding: 4px; 7 | } 8 | 9 | .action-button { 10 | width: 60px; 11 | margin-left: auto; 12 | } 13 | 14 | .browserlist__popup { 15 | --g-color-text-info: var(--g-color-private-color-500-solid); 16 | } 17 | 18 | .browserlist__popup :global(.g-select-list__option) { 19 | gap: 8px; 20 | flex-direction: row-reverse; 21 | } 22 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/CollapsibleSection/index.module.css: -------------------------------------------------------------------------------- 1 | .expand-arrow { 2 | transition: opacity .1s ease, transform .1s ease; 3 | } 4 | 5 | .summary { 6 | cursor: pointer; 7 | user-select: none; 8 | } 9 | 10 | .summary:hover { 11 | opacity: .8; 12 | } 13 | 14 | .expand-arrow--expanded { 15 | transform: rotate(180deg); 16 | } 17 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/CollapsibleSection/utils.ts: -------------------------------------------------------------------------------- 1 | export const getSectionId = (_browserId: string, _attemptIndex: number, id: string): string => { 2 | return id; 3 | }; 4 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 8px 1px 4px calc(var(--indent) * 24px); 5 | row-gap: 8px; 6 | } 7 | 8 | .toolbar-container { 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | gap: 8px; 13 | } 14 | 15 | .toolbar-container > div:only-child { 16 | justify-content: center; 17 | } 18 | 19 | .accept-button { 20 | composes: regular-button from global, action-button from global; 21 | } 22 | 23 | .buttons-container { 24 | display: flex; 25 | margin-left: auto; 26 | } 27 | 28 | .diff-mode-container { 29 | container-type: inline-size; 30 | flex-grow: 1; 31 | display: flex; 32 | } 33 | 34 | .diff-mode-switcher { 35 | --g-color-base-background: #fff; 36 | } 37 | 38 | .diff-mode-select { 39 | display: none; 40 | } 41 | 42 | @container (max-width: 500px) { 43 | .diff-mode-switcher { 44 | display: none !important; 45 | } 46 | 47 | .diff-mode-select { 48 | display: block; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/SnapshotsPlayer/PlayIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // We are using custom icon here, because there are issues with gravity-ui icon in safari due to use of clip path 4 | export const PlayIcon = (): React.ReactNode => ( 5 | 6 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/SnapshotsPlayer/types.ts: -------------------------------------------------------------------------------- 1 | import {eventWithTime} from '@rrweb/types'; 2 | 3 | export type NumberedSnapshot = eventWithTime & {seqNo: number}; 4 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/SortBySelect/index.module.css: -------------------------------------------------------------------------------- 1 | .label-icon-right { 2 | margin-left: -6px; 3 | } 4 | 5 | .option-content { 6 | display: flex; 7 | align-items: center; 8 | gap: 6px; 9 | } 10 | 11 | .option-icon { 12 | color: var(--g-color-private-black-450-solid); 13 | } 14 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/SuitesTreeView/TreeViewSkeleton.module.css: -------------------------------------------------------------------------------- 1 | .skeleton { 2 | height: 24px; 3 | animation: skeleton-appear 1s ease forwards; 4 | animation-delay: var(--delay); 5 | opacity: 0; 6 | } 7 | 8 | @keyframes skeleton-appear { 9 | from {opacity: 0} 10 | to {opacity: 1} 11 | } 12 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TestControlPanel/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | gap: 16px; 4 | } 5 | 6 | .heading { 7 | line-height: 28px; 8 | } 9 | 10 | .attempts-container { 11 | display: flex; 12 | flex-wrap: wrap; 13 | gap: 4px; 14 | } 15 | 16 | .buttons-container { 17 | display: flex; 18 | align-items: center; 19 | gap: 8px; 20 | margin-left: auto; 21 | } 22 | 23 | .divider { 24 | height: 24px; 25 | } 26 | 27 | .retry-button { 28 | composes: regular-button from global, action-button from global; 29 | } 30 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TestInfo/index.module.css: -------------------------------------------------------------------------------- 1 | .steps-container { 2 | align-items: start; 3 | position: relative; 4 | display: flex; 5 | } 6 | 7 | .steps-list-container { 8 | flex-basis: 0; 9 | } 10 | 11 | .empty-steps-container { 12 | align-self: stretch; 13 | justify-content: center; 14 | flex-grow: 1; 15 | display: flex; 16 | align-items: center; 17 | color: var(--g-color-private-black-400); 18 | font-weight: 500; 19 | --g-color-line-brand: var(--g-color-private-black-400); 20 | } 21 | 22 | .sticky { 23 | flex-basis: 0; 24 | flex-shrink: 0; 25 | max-width: 60%; 26 | position: sticky; 27 | top: calc(var(--sticky-header-height) + 45px); 28 | padding: 10px 20px; 29 | } 30 | 31 | .hidden { 32 | display: none; 33 | } 34 | 35 | .collapsible-section__body { 36 | padding: 10px 30px 10px 2px; 37 | word-break: break-all; 38 | } 39 | 40 | .collapsible-section__body a:link, .collapsible-section__body a:visited { 41 | color: var(--color-link); 42 | } 43 | 44 | .collapsible-section__body a:hover { 45 | color: var(--color-link-hover); 46 | } 47 | 48 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TestNameFilter/TestNameFilterButton.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import {Button, Tooltip} from '@gravity-ui/uikit'; 3 | 4 | interface TestNameFilterButtonProps { 5 | children: ReactNode, 6 | selected: boolean, 7 | tooltip: string; 8 | onClick?: () => void; 9 | className?: string 10 | } 11 | 12 | export function TestNameFilterButton(props: TestNameFilterButtonProps): ReactNode { 13 | return 14 | 23 | ; 24 | } 25 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TestNameFilter/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | width: 100%; 4 | } 5 | 6 | .search-input :global(.g-text-input__control) { 7 | padding-right: 54px; 8 | } 9 | 10 | .buttons-wrapper { 11 | position: absolute; 12 | right: 0; 13 | top: 0; 14 | bottom: 0; 15 | display: flex; 16 | gap: 2px; 17 | width: fit-content; 18 | flex-direction: row; 19 | padding: 2px 2px 2px 0; 20 | } 21 | 22 | .buttons-wrapper :global(.g-button) { 23 | color: rgb(113, 113, 122); 24 | } 25 | 26 | .buttons-wrapper :global(.g-button):hover { 27 | color: rgb(9, 9, 11); 28 | } 29 | 30 | .buttons-wrapper :global(.g-button_selected):before { 31 | background-color: var(--g-color-base-simple-hover); 32 | } 33 | 34 | .buttons-wrapper :global(.g-button_selected) { 35 | color: rgb(9, 9, 11); 36 | } 37 | 38 | .buttons-wrapper__regex { 39 | width: var(--_--height); 40 | font-weight: 600; 41 | } 42 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TestStatusFilter/index.module.css: -------------------------------------------------------------------------------- 1 | .test-status-filter { 2 | --g-color-base-background: #fff; 3 | } 4 | 5 | .test-status-filter-option { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | } 10 | 11 | .test-status-filter-option__count { 12 | margin-left: var(--g-spacing-1); 13 | } 14 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TestStepArgs/index.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | flex-wrap: nowrap; 4 | overflow: hidden; 5 | } 6 | 7 | .item { 8 | white-space: nowrap; 9 | min-width: 0; 10 | overflow: hidden; 11 | text-overflow: ellipsis; 12 | max-width: max-content; 13 | 14 | border-radius: 4px; 15 | font-size: 16px; 16 | color: var(--color-tag-title, var(--g-color-private-black-400)); 17 | background-color: var(--color-tag-bg, var(--g-color-base-generic)); 18 | margin-left: 8px; 19 | padding: 0 4px; 20 | } 21 | 22 | .item--failed { 23 | background: var(--g-color-private-red-50); 24 | color: var(--g-color-private-red-500-solid); 25 | } 26 | 27 | .collapse-first { 28 | flex-shrink: 0; 29 | flex-grow: 1; 30 | flex-basis: 0; 31 | min-width: 0; 32 | } 33 | 34 | .collapse-second { 35 | flex-shrink: 1; 36 | flex-grow: 0; 37 | } 38 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TestStepArgs/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, {ReactNode} from 'react'; 3 | 4 | import styles from './index.module.css'; 5 | import {stringify} from '@/static/new-ui/utils'; 6 | 7 | interface TestStepArgsProps { 8 | args: string[]; 9 | isFailed?: boolean; 10 | isActive?: boolean; 11 | } 12 | 13 | export function TestStepArgs(props: TestStepArgsProps): ReactNode { 14 | const renderItems = (index: number): ReactNode => { 15 | if (index >= props.args.length) { 16 | return null; 17 | } 18 | 19 | return
20 | {stringify(props.args[index])} 23 | {renderItems(index + 1)} 24 |
; 25 | }; 26 | 27 | return renderItems(0); 28 | } 29 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TestSteps/index.module.css: -------------------------------------------------------------------------------- 1 | .step-content { 2 | display: flex; 3 | align-items: center; 4 | min-width: 0; 5 | } 6 | 7 | .step-args-wrapper { 8 | display: flex; 9 | min-width: 0; 10 | } 11 | 12 | .error-info { 13 | color: red; 14 | margin: 8px 0; 15 | } 16 | 17 | .page-screenshot { 18 | margin: 8px 0; 19 | padding-left: 24px; 20 | padding-right: 1px; 21 | } 22 | 23 | .step-duration { 24 | margin-left: auto; 25 | padding: 0 8px; 26 | font-size: 14px; 27 | line-height: 14px; 28 | opacity: .4; 29 | white-space: nowrap; 30 | } 31 | 32 | .step { 33 | transition: opacity .3s ease; 34 | } 35 | 36 | .step--dimmed { 37 | opacity: 0.4; 38 | } 39 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TestSteps/utils.ts: -------------------------------------------------------------------------------- 1 | import {unstable_ListTreeItemType as ListTreeItemType, unstable_UseListResult as UseListResult} from '@gravity-ui/uikit/unstable'; 2 | import React from 'react'; 3 | 4 | export const traverseTree = (treeItems: ListTreeItemType[], cb: (item: ListTreeItemType) => unknown): void => { 5 | function dfs(step: ListTreeItemType): void { 6 | cb(step); 7 | 8 | if (step.children) { 9 | for (const child of step.children) { 10 | dfs(child); 11 | } 12 | } 13 | } 14 | 15 | for (const step of treeItems) { 16 | dfs(step); 17 | } 18 | }; 19 | 20 | export const getIndentStyle = (list: UseListResult, id: string): React.CSSProperties => { 21 | return {'--indent': list.structure.itemsState[id].indentation} as React.CSSProperties; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.module.css: -------------------------------------------------------------------------------- 1 | .tree-view-item-subtitle__error-stack { 2 | white-space: pre; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | line-height: 18px; 6 | margin-top: 8px; 7 | 8 | border-radius: 5px; 9 | margin-right: 8px; 10 | margin-bottom: 4px; 11 | padding: 8px; 12 | font-family: monospace; 13 | font-size: 13px; 14 | border: 1px solid #ff000024; 15 | 16 | background-color: var(--g-color-private-red-50); 17 | } 18 | 19 | .image-status { 20 | font-size: 15px; 21 | word-break: break-word; 22 | } 23 | 24 | .skip-reason-container { 25 | font-size: 15px; 26 | overflow: hidden; 27 | } 28 | 29 | .skip-reason { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | .skip-reason-container a:link, .skip-reason-container a:visited { 35 | color: var(--color-link); 36 | } 37 | 38 | .skip-reason-container a:hover { 39 | color: var(--color-link-hover); 40 | } 41 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/suites/constants.ts: -------------------------------------------------------------------------------- 1 | export const MIN_SECTION_SIZE_PERCENT = 25; 2 | -------------------------------------------------------------------------------- /lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .card { 8 | height: 100%; 9 | } 10 | 11 | .card__title { 12 | margin-bottom: 8px; 13 | padding-top: 20px; 14 | } 15 | 16 | .toolbar-container { 17 | --g-color-text-primary: var(--g-color-private-cool-grey-700-solid); 18 | color: var(--g-color-private-cool-grey-700-solid); 19 | display: flex; 20 | gap: 16px; 21 | margin-bottom: 8px; 22 | } 23 | 24 | .accept-button { 25 | composes: regular-button from global, action-button from global; 26 | } 27 | 28 | .buttons-container { 29 | margin-left: auto; 30 | } 31 | 32 | .hint { 33 | align-items: center; 34 | color: var(--g-color-private-black-400); 35 | display: flex; 36 | flex-grow: 1; 37 | font-weight: 500; 38 | justify-content: center; 39 | } 40 | -------------------------------------------------------------------------------- /lib/static/new-ui/hooks/useAnalytics.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | 3 | import {AnalyticsContext} from '@/static/new-ui/providers/analytics'; 4 | import {YandexMetrika} from '@/static/modules/yandex-metrika'; 5 | import {NEW_ISSUE_LINK} from '@/constants'; 6 | 7 | export const useAnalytics = (): YandexMetrika | null => { 8 | const analytics = useContext(AnalyticsContext); 9 | 10 | if (!analytics) { 11 | console.warn('Failed to get analytics class instance to send usage info. If you are a user, you can safely ignore this. Feel free to report it to use at ' + NEW_ISSUE_LINK); 12 | } 13 | 14 | return analytics; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/static/new-ui/providers/analytics.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, ReactNode, useMemo} from 'react'; 2 | import {YandexMetrika} from '@/static/modules/yandex-metrika'; 3 | import {getAreAnalyticsEnabled, getCounterId} from '@/static/new-ui/utils/analytics'; 4 | 5 | export const AnalyticsContext = createContext(null); 6 | 7 | interface AnalyticsProviderProps { 8 | children: React.ReactNode; 9 | } 10 | 11 | export const AnalyticsProvider = ({children}: AnalyticsProviderProps): ReactNode => { 12 | const areAnalyticsEnabled = getAreAnalyticsEnabled(); 13 | const counterId = getCounterId(); 14 | const analytics = useMemo(() => new YandexMetrika(areAnalyticsEnabled, counterId), []); 15 | 16 | return 17 | {children} 18 | ; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/static/new-ui/providers/event-source.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, ReactNode, useContext, useEffect, useState} from 'react'; 2 | 3 | const EventSourceContext = createContext(null); 4 | 5 | interface EventSourceProviderProps { 6 | children: ReactNode; 7 | } 8 | 9 | export const EventSourceProvider = ({children}: EventSourceProviderProps): ReactNode => { 10 | const [eventSource, setEventSource] = useState(null); 11 | 12 | useEffect(() => { 13 | const es = new EventSource('/events'); 14 | setEventSource(es); 15 | 16 | return () => { 17 | es.close(); 18 | }; 19 | }, []); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export const useEventSource = (): EventSource | null => { 29 | return useContext(EventSourceContext); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/static/new-ui/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Point { 2 | x: number; 3 | y: number; 4 | } 5 | -------------------------------------------------------------------------------- /lib/static/new-ui/types/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const value: string; 3 | export = value; 4 | } 5 | 6 | declare module '*.module.css' { 7 | const classes: {[key: string]: string}; 8 | export default classes; 9 | } 10 | 11 | declare module 'ansi-html-community' { 12 | interface AnsiHtmlCommunity { 13 | (value: string): string; 14 | setColors: (colors: Record) => void; 15 | } 16 | const f: AnsiHtmlCommunity; 17 | 18 | export default f; 19 | } 20 | -------------------------------------------------------------------------------- /lib/static/new-ui/utils/analytics.ts: -------------------------------------------------------------------------------- 1 | import {DataForStaticFile} from '@/server-utils'; 2 | 3 | declare global { 4 | interface Window { 5 | data?: DataForStaticFile 6 | } 7 | } 8 | 9 | export const getAreAnalyticsEnabled = (): boolean => { 10 | const metrikaConfig = (window.data || {}).config?.yandexMetrika; 11 | 12 | return Boolean(metrikaConfig?.enabled && metrikaConfig?.counterNumber); 13 | }; 14 | 15 | export const getCounterId = (): number => { 16 | const metrikaConfig = (window.data || {}).config?.yandexMetrika; 17 | 18 | return metrikaConfig?.counterNumber ?? 0; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/static/new-ui/utils/api.ts: -------------------------------------------------------------------------------- 1 | import {ApiErrorResponse, UpdateTimeTravelSettingsRequest, UpdateTimeTravelSettingsResponse} from '@/types'; 2 | 3 | export const isApiErrorResponse = (response: UpdateTimeTravelSettingsResponse): response is ApiErrorResponse => { 4 | return 'error' in response; 5 | }; 6 | 7 | export const updateTimeTravelSettings = async (params: UpdateTimeTravelSettingsRequest): Promise => { 8 | return fetch('/update-time-travel-settings', { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json' 12 | }, 13 | body: JSON.stringify(params) 14 | }).then(response => response.json()); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "include": ["."], 4 | "compilerOptions": { 5 | "jsx": "react", 6 | "noEmit": true, 7 | "moduleResolution": "bundler", 8 | "paths": { 9 | "@/*": ["../*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/static/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --report-color-fail: #d00; 3 | --report-color-skip: #8c8c8c; 4 | --report-color-success: #169737; 5 | --report-color-staged: #bd5c0a; 6 | --report-color-commited: #3072b3; 7 | 8 | --button-border-radius: 6px; 9 | --button-action-color: #ffeba0; 10 | --button-hover-color: #9a9999; 11 | 12 | --text-color: rgba(0, 0, 0, .65) 13 | } 14 | -------------------------------------------------------------------------------- /lib/workers/create-workers.ts: -------------------------------------------------------------------------------- 1 | import type {EventEmitter} from 'events'; 2 | 3 | type MapOfMethods> = { 4 | [K in T[number]]: (...args: Array) => Promise | unknown; 5 | }; 6 | 7 | export type RegisterWorkers> = EventEmitter & MapOfMethods; 8 | 9 | export const createWorkers = ( 10 | runner: {registerWorkers: (workerFilePath: string, exportedMethods: string[]) => RegisterWorkers<['saveDiffTo']>} 11 | ): RegisterWorkers<['saveDiffTo']> => { 12 | const workerFilepath = require.resolve('./worker'); 13 | 14 | return runner.registerWorkers(workerFilepath, ['saveDiffTo']); 15 | }; 16 | 17 | export type CreateWorkersRunner = Parameters[0]; 18 | -------------------------------------------------------------------------------- /lib/workers/worker.ts: -------------------------------------------------------------------------------- 1 | import looksSame from 'looks-same'; 2 | import {DiffOptions} from '../types'; 3 | 4 | export function saveDiffTo(diffOpts: DiffOptions, diffPath: string): Promise { 5 | const {diffColor: highlightColor, ...otherOpts} = diffOpts; 6 | 7 | return looksSame.createDiff({diff: diffPath, highlightColor, ...otherOpts}); 8 | } 9 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'gemini-testing/tests' 5 | }; 6 | -------------------------------------------------------------------------------- /test/func/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'gemini-testing/tests', 5 | env: {browser: true}, 6 | overrides: [ 7 | { 8 | files: ['tests/**/*.testplane.js', 'fixtures/**/*.testplane.js'], 9 | globals: { 10 | expect: 'readonly' 11 | } 12 | } 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /test/func/common.testplane.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const {GRID_URL, CHROME_BINARY_PATH} = require('./utils/constants'); 4 | 5 | module.exports.getCommonConfig = (projectDir) => ({ 6 | gridUrl: GRID_URL, 7 | 8 | screenshotsDir: path.resolve(projectDir, 'screens'), 9 | 10 | browsers: { 11 | chrome: { 12 | assertViewOpts: { 13 | ignoreDiffPixelCount: 4 14 | }, 15 | windowSize: '1280x1024', 16 | desiredCapabilities: { 17 | browserName: 'chrome', 18 | 'goog:chromeOptions': { 19 | args: ['headless', 'no-sandbox', 'hide-scrollbars', 'disable-dev-shm-usage'], 20 | binary: CHROME_BINARY_PATH 21 | } 22 | }, 23 | waitTimeout: 3000 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /test/func/docker/.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /test/func/fixtures/analytics/disabled.testplane.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const {getFixturesConfig} = require('../fixtures.testplane.conf'); 6 | 7 | module.exports = _.merge(getFixturesConfig(__dirname), { 8 | plugins: { 9 | 'html-reporter-tester': { 10 | baseHost: 'https://example.com:123', 11 | yandexMetrika: { 12 | enabled: false 13 | } 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /test/func/fixtures/analytics/enabled.testplane.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const {getFixturesConfig} = require('../fixtures.testplane.conf'); 6 | 7 | module.exports = _.merge(getFixturesConfig(__dirname), { 8 | plugins: { 9 | 'html-reporter-tester': { 10 | baseHost: 'https://example.com:123' 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /test/func/fixtures/analytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "analytics", 3 | "description": "Test project that generates html-report for testing analytics", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "clean": "rm -rf report", 8 | "generate": "true # This report should be generated with different env vars during tests", 9 | "gui": "npx testplane gui --hostname 0.0.0.0 --port $(../../utils/get-port.js analytics gui)" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/func/fixtures/analytics/test.testplane.js: -------------------------------------------------------------------------------- 1 | it('some test', async () => { 2 | throw new Error('Test should fail'); 3 | }); 4 | -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/.testplane.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const {getFixturesConfig} = require('../fixtures.testplane.conf'); 6 | 7 | module.exports = _.merge(getFixturesConfig(__dirname, 'testplane'), { 8 | plugins: { 9 | 'hermione-test-repeater': { 10 | enabled: true, 11 | repeat: 1 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "db-migrations-fixture-report", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "clean": "rm -rf report", 7 | "generate": "true # This fixture should not be generated. Report is commited to VCS so that sqlite.db is fixed at v0. Used to test html-reporter backwards compatibility with old sqlite formats.", 8 | "gui": "npx testplane gui --hostname 0.0.0.0 --port $(../../utils/get-port.js db-migrations gui)" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/data.js: -------------------------------------------------------------------------------- 1 | var data = {"skips":[],"config":{"defaultView":"all","diffMode":"3-up","baseHost":"","errorPatterns":[],"metaInfoBaseUrls":{},"customScripts":[],"yandexMetrika":{"enabled":true,"counterNumber":99267510},"pluginsEnabled":false,"plugins":[],"staticImageAccepter":{"enabled":false,"repositoryUrl":"","pullRequestUrl":"","serviceUrl":"","meta":{},"axiosRequestOptions":{}}},"apiValues":{"toolName":"testplane","extraItems":{},"metaInfoExtenders":{},"imagesSaver":{"saveImg":"async (srcCurrPath, { destPath, reportDir }) => {\n await (0, server_utils_1.copyFileAsync)(srcCurrPath, destPath, { reportDir });\n return destPath;\n }"},"reportsSaver":null},"timestamp":1746278994956,"date":"Sat May 03 2025 16:29:54 GMT+0300 (Moscow Standard Time)"}; 2 | try { module.exports = data; } catch(e) {} -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/databaseUrls.json: -------------------------------------------------------------------------------- 1 | {"dbUrls":["sqlite.db"],"jsonUrls":[]} 2 | -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/icons/broken-snapshot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/icons/exclamation-triangle-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/icons/favicon-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/db-migrations/report/icons/favicon-failure.png -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/icons/favicon-running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/db-migrations/report/icons/favicon-running.png -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/icons/favicon-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/db-migrations/report/icons/favicon-success.png -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/db-migrations/report/icons/favicon.png -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/icons/testplane-mono.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/sql-wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/db-migrations/report/sql-wasm.wasm -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/report/sqlite.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/db-migrations/report/sqlite.db -------------------------------------------------------------------------------- /test/func/fixtures/db-migrations/success-describe.testplane.js: -------------------------------------------------------------------------------- 1 | describe('success describe', function() { 2 | it('successfully passed test', async ({browser}) => { 3 | await browser.url(browser.options.baseUrl); 4 | 5 | assert.isTrue(true); 6 | }); 7 | 8 | it('test with screenshot', async ({browser}) => { 9 | await browser.url(browser.options.baseUrl); 10 | 11 | await browser.assertView('header', 'header', {ignoreDiffPixelCount: '100%'}); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/func/fixtures/playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-fixture-report", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "generate": "npx playwright test" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/func/fixtures/playwright/tests/screens/failed-describe-test-with-diff/chromium/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-diff/chromium/header.png -------------------------------------------------------------------------------- /test/func/fixtures/playwright/tests/screens/failed-describe-test-with-image-comparison-diff/chromium/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-image-comparison-diff/chromium/header.png -------------------------------------------------------------------------------- /test/func/fixtures/playwright/tests/screens/failed-describe-test-with-successful-assertView-and-error/chromium/header-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-successful-assertView-and-error/chromium/header-success.png -------------------------------------------------------------------------------- /test/func/fixtures/playwright/tests/screens/failed-describe-test-without-screenshot/chromium/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/playwright/tests/screens/failed-describe-test-without-screenshot/chromium/header.png -------------------------------------------------------------------------------- /test/func/fixtures/playwright/tests/screens/success-describe-test-with-screenshot/chromium/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/playwright/tests/screens/success-describe-test-with-screenshot/chromium/header.png -------------------------------------------------------------------------------- /test/func/fixtures/playwright/tests/success-describe.spec.ts: -------------------------------------------------------------------------------- 1 | import {test} from '@playwright/test'; 2 | 3 | test.describe('success describe', () => { 4 | test('successfully passed test', async ({page, baseURL}) => { 5 | await page.goto(baseURL as string); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/func/fixtures/plugins/failed-describe.testplane.js: -------------------------------------------------------------------------------- 1 | describe('failed describe', function() { 2 | it('successfully passed test', async ({browser}) => { 3 | await browser.url(browser.options.baseUrl); 4 | 5 | assert.isTrue(true); 6 | }); 7 | 8 | it('test without screenshot', async ({browser}) => { 9 | await browser.url(browser.options.baseUrl); 10 | 11 | await browser.assertView('header', 'header'); 12 | }); 13 | 14 | it('test with long error message', async () => { 15 | throw new Error(`long_error_message ${'0123456789'.repeat(20)}\n message content`); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/func/fixtures/plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugins-fixture-report", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "clean": "rm -rf report", 7 | "generate": "npx testplane", 8 | "gui": "npx testplane gui --hostname 0.0.0.0 --port 8002" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/func/fixtures/plugins/screens/eea1754/chrome/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/plugins/screens/eea1754/chrome/header.png -------------------------------------------------------------------------------- /test/func/fixtures/plugins/success-describe.testplane.js: -------------------------------------------------------------------------------- 1 | describe('success describe', function() { 2 | it('successfully passed test', async ({browser}) => { 3 | await browser.url(browser.options.baseUrl); 4 | 5 | assert.isTrue(true); 6 | }); 7 | 8 | it('test with screenshot', async ({browser}) => { 9 | await browser.url(browser.options.baseUrl); 10 | 11 | await browser.assertView('header', 'header', {ignoreDiffPixelCount: '100%'}); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-eye/.testplane.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const {getFixturesConfig} = require('../fixtures.testplane.conf'); 6 | 7 | module.exports = _.merge(getFixturesConfig(__dirname), { 8 | plugins: { 9 | 'html-reporter-tester': { 10 | baseHost: 'https://example.com:123' 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-eye/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello, world

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-eye/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testplane-eye", 3 | "description": "Test project that generates html-report for testing the view in browser (eye button) feature", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "clean": "rm -rf report", 8 | "generate": "npx testplane", 9 | "gui": "npx testplane gui --hostname 0.0.0.0 --port $(../../utils/get-port.js testplane-eye gui)" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-eye/test.testplane.js: -------------------------------------------------------------------------------- 1 | describe('success describe', function() { 2 | it('successfully passed test', async ({browser}) => { 3 | await browser.url(browser.options.baseUrl); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-gui/.testplane.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const {getFixturesConfig} = require('../fixtures.testplane.conf'); 6 | 7 | module.exports = _.merge(getFixturesConfig(__dirname), {}); 8 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-gui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello, world!

9 |

Another line...

10 |

The third line.

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-gui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testplane-gui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "clean": "rm -rf report", 7 | "generate": "npx testplane --grep 'tests to run'", 8 | "gui": "npx testplane gui --hostname 0.0.0.0 --port $(../../utils/get-port.js testplane-gui gui)" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-gui/screens/442f53a/chrome/paragraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/testplane-gui/screens/442f53a/chrome/paragraph.png -------------------------------------------------------------------------------- /test/func/fixtures/testplane-gui/test.testplane.js: -------------------------------------------------------------------------------- 1 | describe('tests to run', () => { 2 | it('test with image comparison diff', async ({browser}) => { 3 | await browser.url(browser.options.baseUrl); 4 | 5 | await browser.assertView('paragraph', '#paragraph-1'); 6 | }); 7 | 8 | it('test with no reference image', async ({browser}) => { 9 | await browser.url(browser.options.baseUrl); 10 | 11 | await browser.assertView('paragraph', '#paragraph-3'); 12 | }); 13 | }); 14 | 15 | describe('tests not to run', () => { 16 | it('successful test', async ({browser}) => { 17 | await browser.url(browser.options.baseUrl); 18 | 19 | assert.isTrue(true); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-tinder/.testplane.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const {getFixturesConfig} = require('../fixtures.testplane.conf'); 6 | 7 | module.exports = _.merge(getFixturesConfig(__dirname), {}); 8 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-tinder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello, world!

9 |

Another line...

10 |

The third line.

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-tinder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testplane-tinder", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "clean": "rm -rf report", 7 | "generate": "npx testplane --grep 'tests to run'", 8 | "gui": "npx testplane gui --hostname 0.0.0.0 --port $(../../utils/get-port.js testplane-tinder gui)" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane-tinder/screens/442f53a/chrome/paragraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/testplane-tinder/screens/442f53a/chrome/paragraph.png -------------------------------------------------------------------------------- /test/func/fixtures/testplane-tinder/test.testplane.js: -------------------------------------------------------------------------------- 1 | describe('tests to run', () => { 2 | it('test with image comparison diff', async ({browser}) => { 3 | await browser.url(browser.options.baseUrl); 4 | 5 | await browser.assertView('paragraph', '#paragraph-1'); 6 | }); 7 | 8 | it('test with no reference image', async ({browser}) => { 9 | await browser.url(browser.options.baseUrl); 10 | 11 | await browser.assertView('paragraph', '#paragraph-3'); 12 | }); 13 | }); 14 | 15 | describe('tests not to run', () => { 16 | it('successful test', async ({browser}) => { 17 | await browser.url(browser.options.baseUrl); 18 | 19 | assert.isTrue(true); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane/.testplane.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const {getFixturesConfig} = require('../fixtures.testplane.conf'); 6 | 7 | module.exports = _.merge(getFixturesConfig(__dirname, 'testplane'), { 8 | plugins: { 9 | 'hermione-test-repeater': { 10 | enabled: true, 11 | repeat: 1 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testplane-fixture-report", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "clean": "rm -rf report", 7 | "generate": "npx testplane", 8 | "gui": "npx testplane gui --hostname 0.0.0.0 --port 8001" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/func/fixtures/testplane/screens/7357338/chrome/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/testplane/screens/7357338/chrome/header.png -------------------------------------------------------------------------------- /test/func/fixtures/testplane/screens/ba3c69a/chrome/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/testplane/screens/ba3c69a/chrome/header.png -------------------------------------------------------------------------------- /test/func/fixtures/testplane/screens/eea1754/chrome/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/testplane/screens/eea1754/chrome/header.png -------------------------------------------------------------------------------- /test/func/fixtures/testplane/screens/f0c3ac2/chrome/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/fixtures/testplane/screens/f0c3ac2/chrome/header.png -------------------------------------------------------------------------------- /test/func/fixtures/testplane/success-describe.testplane.js: -------------------------------------------------------------------------------- 1 | describe('success describe', function() { 2 | it('successfully passed test', async ({browser}) => { 3 | await browser.url(browser.options.baseUrl); 4 | 5 | assert.isTrue(true); 6 | }); 7 | 8 | it('test with screenshot', async ({browser}) => { 9 | await browser.url(browser.options.baseUrl); 10 | 11 | await browser.assertView('header', 'header', {ignoreDiffPixelCount: '100%'}); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/func/packages/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: {browser: true}, 3 | plugins: [ 4 | 'react' 5 | ], 6 | rules: { 7 | 'react/jsx-uses-react': 'error', 8 | 'react/jsx-uses-vars': 'error', 9 | 'react/jsx-no-undef': ['error', {allowGlobals: true}] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/func/packages/basic/lib/color-border.css: -------------------------------------------------------------------------------- 1 | .red-border { 2 | border: 10px solid red; 3 | } 4 | 5 | .green-border { 6 | border: 10px solid green; 7 | } 8 | 9 | .blue-border { 10 | border: 10px solid blue; 11 | } 12 | -------------------------------------------------------------------------------- /test/func/packages/basic/lib/color-border.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PropTypes from 'prop-types'; 4 | import './color-border.css'; 5 | 6 | const nextColors = { 7 | 'red': 'green', 8 | 'green': 'blue', 9 | 'blue': 'red' 10 | }; 11 | 12 | export class ColorBorder extends React.Component { 13 | static propTypes = { 14 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]) 15 | }; 16 | 17 | state = {color: 'red'}; 18 | 19 | onBorderClick = (e) => { 20 | e.stopPropagation(); 21 | this.setState(function(state) { 22 | return { 23 | color: nextColors[state.color] 24 | }; 25 | }); 26 | }; 27 | 28 | render() { 29 | const className = `${this.state.color}-border basic-border`; 30 | return
31 | {this.props.children} 32 |
; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/func/packages/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-reporter-basic-plugin", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "webpack --config webpack.config.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/func/packages/basic/webpack.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const commonConfig = require('../webpack.common'); 3 | 4 | module.exports = merge(commonConfig, { 5 | entry: { 6 | plugin: './lib/color-border.jsx' 7 | }, 8 | output: { 9 | path: __dirname 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /test/func/packages/html-reporter-test-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-reporter-test-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /test/func/packages/html-reporter-tester/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('../../../../build/testplane'); 4 | -------------------------------------------------------------------------------- /test/func/packages/html-reporter-tester/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-reporter-tester", 3 | "version": "1.0.0", 4 | "description": "", 5 | "exports": { 6 | ".": "./index.js", 7 | "./playwright": "./playwright.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /test/func/packages/html-reporter-tester/playwright.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('../../../../build/playwright'); 4 | -------------------------------------------------------------------------------- /test/func/packages/menu-bar/lib/menu-bar-item.css: -------------------------------------------------------------------------------- 1 | .menu-item__link.menu-bar-item { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/func/packages/menu-bar/lib/menu-bar-item.jsx: -------------------------------------------------------------------------------- 1 | import './menu-bar-item.css'; 2 | 3 | export default ['react', 'semantic-ui-react', function(React, {Dropdown}, {pluginName}) { 4 | class MenuBarItem extends React.Component { 5 | // allow the component to be placed only on "menu-bar" extension point 6 | static point = 'menu-bar'; 7 | 8 | render() { 9 | return ( 10 | 11 | {pluginName} 12 | 13 | ); 14 | } 15 | } 16 | 17 | return { 18 | MenuBarItem 19 | }; 20 | }]; 21 | -------------------------------------------------------------------------------- /test/func/packages/menu-bar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-reporter-menu-bar-plugin", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "webpack --config webpack.config.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/func/packages/menu-bar/webpack.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const commonConfig = require('../webpack.common'); 3 | 4 | module.exports = merge(commonConfig, { 5 | entry: { 6 | plugin: './lib/menu-bar-item.jsx' 7 | }, 8 | output: { 9 | path: __dirname 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /test/func/packages/redux-with-server/lib/color-border.css: -------------------------------------------------------------------------------- 1 | .red-border { 2 | border: 10px solid red; 3 | } 4 | 5 | .green-border { 6 | border: 10px solid green; 7 | } 8 | 9 | .blue-border { 10 | border: 10px solid blue; 11 | } 12 | -------------------------------------------------------------------------------- /test/func/packages/redux-with-server/middleware.js: -------------------------------------------------------------------------------- 1 | module.exports = function(pluginRouter) { 2 | const nextColors = { 3 | 'red': 'green', 4 | 'green': 'blue', 5 | 'blue': 'red' 6 | }; 7 | 8 | pluginRouter.get('/color', function(req, res) { 9 | const currentColor = req.query.color || 'red'; 10 | res.send({color: nextColors[currentColor]}); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /test/func/packages/redux-with-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-reporter-redux-with-server-plugin", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "webpack --config webpack.config.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/func/packages/redux-with-server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const commonConfig = require('../webpack.common'); 3 | 4 | module.exports = merge(commonConfig, { 5 | entry: { 6 | plugin: './lib/color-border.jsx' 7 | }, 8 | output: { 9 | path: __dirname 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /test/func/packages/redux/lib/color-border.css: -------------------------------------------------------------------------------- 1 | .red-border { 2 | border: 10px solid red; 3 | } 4 | 5 | .green-border { 6 | border: 10px solid green; 7 | } 8 | 9 | .blue-border { 10 | border: 10px solid blue; 11 | } 12 | -------------------------------------------------------------------------------- /test/func/packages/redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-reporter-redux-plugin", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "webpack --config webpack.config.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/func/packages/redux/webpack.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const commonConfig = require('../webpack.common'); 3 | 4 | module.exports = merge(commonConfig, { 5 | entry: { 6 | plugin: './lib/color-border.jsx' 7 | }, 8 | output: { 9 | path: __dirname 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /test/func/tests/common/error-group.testplane.js: -------------------------------------------------------------------------------- 1 | describe(process.env.TOOL || 'Default', () => { 2 | describe('Error grouping', function() { 3 | it('should group errors', async ({browser}) => { 4 | const groupByDropdown = await browser.$('[data-qa="group-by-dropdown"]'); 5 | 6 | await groupByDropdown.click(); 7 | 8 | await browser.$('div=error').click(); 9 | 10 | const groupedTestsContainer = await browser.$('.grouped-tests'); 11 | 12 | const longErrorMessageGroup = await groupedTestsContainer.$('span*=long_error_message').$('..'); 13 | 14 | await expect(longErrorMessageGroup).toBeDisplayed(); 15 | await expect(await longErrorMessageGroup.$('span*=tests: 1')).toBeDisplayed(); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/func/tests/local.testplane.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | This testplane config may be useful for running tests on a local, non-headless Chromium browser while debugging. 3 | 4 | Use it as follows: 5 | npm run gui:testplane-common -- -c local.testplane.conf.js 6 | */ 7 | 8 | process.env.SERVER_HOST = 'localhost'; 9 | 10 | const _ = require('lodash'); 11 | 12 | const mainConfig = require('./.testplane.conf.js'); 13 | 14 | const config = _.merge(mainConfig, { 15 | browsers: { 16 | chrome: { 17 | automationProtocol: 'devtools', 18 | desiredCapabilities: { 19 | 'goog:chromeOptions': { 20 | args: ['no-sandbox', 'hide-scrollbars'] 21 | } 22 | }, 23 | waitTimeout: 3000 24 | } 25 | } 26 | }); 27 | 28 | delete config.gridUrl; 29 | delete config.browsers.chrome.desiredCapabilities['goog:chromeOptions'].binary; 30 | 31 | module.exports = config; 32 | -------------------------------------------------------------------------------- /test/func/tests/plugins/tests-menu-bar-plugin.testplane.js: -------------------------------------------------------------------------------- 1 | describe('Test menu bar plugin', function() { 2 | const selector = '.menu-bar__dropdown'; 3 | 4 | it('should show menu bar with plugins applied', async ({browser}) => { 5 | await browser.$(selector).waitForDisplayed(); 6 | await browser.assertView('menu bar plugins', selector); 7 | }); 8 | 9 | it('should show menu bar item on click', async ({browser}) => { 10 | const menuSelector = '.menu-bar__content'; 11 | 12 | await browser.$(selector).waitForDisplayed(); 13 | await browser.$(selector).click(); 14 | await browser.$(menuSelector).waitForDisplayed(); 15 | 16 | // Pause prevents flaky screenshots due to dropdown shadow rendering. No, screenshot delay doesn't help. 17 | await browser.pause(1000); 18 | await browser.assertView('menu bar plugins clicked', [selector, menuSelector]); 19 | }); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /test/func/tests/plugins/tests-redux-plugin.testplane.js: -------------------------------------------------------------------------------- 1 | const {mkNestedSelector} = require('../../utils'); 2 | 3 | describe('Test redux plugin', function() { 4 | it('should change plugin redux border color on click', async ({browser}) => { 5 | const screenSelector = mkNestedSelector( 6 | '.section .section_status_error', 7 | '.section .section__body' 8 | ); 9 | 10 | const clickSelector = mkNestedSelector( 11 | '.section .section_status_error', 12 | '.red-border.redux-border' 13 | ); 14 | 15 | await browser.$(screenSelector).waitForDisplayed(); 16 | await browser.$(clickSelector).click(); 17 | // Letting browser handle re-renders 18 | await browser.pause(1000); 19 | await browser.assertView('redux plugin clicked', screenSelector); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/func/tests/screens/0049570/chrome/retry-switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/0049570/chrome/retry-switcher.png -------------------------------------------------------------------------------- /test/func/tests/screens/07c99c0/chrome/retry-switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/07c99c0/chrome/retry-switcher.png -------------------------------------------------------------------------------- /test/func/tests/screens/1bb949f/chrome/retry-switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/1bb949f/chrome/retry-switcher.png -------------------------------------------------------------------------------- /test/func/tests/screens/2df3350/chrome/retry-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/2df3350/chrome/retry-selector.png -------------------------------------------------------------------------------- /test/func/tests/screens/3144090/chrome/menu bar plugins clicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/3144090/chrome/menu bar plugins clicked.png -------------------------------------------------------------------------------- /test/func/tests/screens/45b9477/chrome/retry-switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/45b9477/chrome/retry-switcher.png -------------------------------------------------------------------------------- /test/func/tests/screens/5c90021/chrome/basic plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/5c90021/chrome/basic plugins.png -------------------------------------------------------------------------------- /test/func/tests/screens/6551ff5/chrome/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/6551ff5/chrome/button.png -------------------------------------------------------------------------------- /test/func/tests/screens/6551ff5/chrome/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/6551ff5/chrome/tooltip.png -------------------------------------------------------------------------------- /test/func/tests/screens/67cd8d8/chrome/retry-switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/67cd8d8/chrome/retry-switcher.png -------------------------------------------------------------------------------- /test/func/tests/screens/683f0cf/chrome/retry-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/683f0cf/chrome/retry-selector.png -------------------------------------------------------------------------------- /test/func/tests/screens/696c6c6/chrome/details summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/696c6c6/chrome/details summary.png -------------------------------------------------------------------------------- /test/func/tests/screens/6a4b847/chrome/retry-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/6a4b847/chrome/retry-selector.png -------------------------------------------------------------------------------- /test/func/tests/screens/9679255/chrome/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/9679255/chrome/button.png -------------------------------------------------------------------------------- /test/func/tests/screens/9679255/chrome/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/9679255/chrome/tooltip.png -------------------------------------------------------------------------------- /test/func/tests/screens/972e9ff/chrome/menu bar plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/972e9ff/chrome/menu bar plugins.png -------------------------------------------------------------------------------- /test/func/tests/screens/a8c2699/chrome/retry-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/a8c2699/chrome/retry-selector.png -------------------------------------------------------------------------------- /test/func/tests/screens/bb1ceae/chrome/details summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/bb1ceae/chrome/details summary.png -------------------------------------------------------------------------------- /test/func/tests/screens/bc098ae/chrome/retry-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/bc098ae/chrome/retry-selector.png -------------------------------------------------------------------------------- /test/func/tests/screens/bdf4a21/chrome/retry-switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/bdf4a21/chrome/retry-switcher.png -------------------------------------------------------------------------------- /test/func/tests/screens/be4ff5b/chrome/basic plugins clicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/be4ff5b/chrome/basic plugins clicked.png -------------------------------------------------------------------------------- /test/func/tests/screens/befc47b/chrome/retry-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/befc47b/chrome/retry-selector.png -------------------------------------------------------------------------------- /test/func/tests/screens/cfcb171/chrome/retry-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/cfcb171/chrome/retry-selector.png -------------------------------------------------------------------------------- /test/func/tests/screens/d8c5b8a/chrome/redux plugin clicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/d8c5b8a/chrome/redux plugin clicked.png -------------------------------------------------------------------------------- /test/func/tests/screens/e219988/chrome/section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/e219988/chrome/section.png -------------------------------------------------------------------------------- /test/func/tests/screens/f23f882/chrome/retry-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/f23f882/chrome/retry-selector.png -------------------------------------------------------------------------------- /test/func/tests/screens/f8fe878/chrome/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/f8fe878/chrome/button.png -------------------------------------------------------------------------------- /test/func/tests/screens/f8fe878/chrome/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemini-testing/html-reporter/67e76665ba366a23fa9d96eef414e01f9b8fc5d0/test/func/tests/screens/f8fe878/chrome/tooltip.png -------------------------------------------------------------------------------- /test/func/tests/static-server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const dir = process.env.STATIC_DIR; 4 | const port = process.env.PORT; 5 | const host = process.env.HOST ?? 'localhost'; 6 | 7 | const app = express(); 8 | app.use(express.static(dir)); 9 | app.listen(port, (err) => { 10 | if (err) { 11 | console.error('Failed to start test server:'); 12 | throw new Error(err); 13 | } 14 | 15 | process.send('Ready'); 16 | console.info(`Server is listening on ${host}:${port}`); 17 | }); 18 | -------------------------------------------------------------------------------- /test/func/utils/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | GRID_URL: 'http://127.0.0.1:4444/', 3 | CHROME_BINARY_PATH: '/usr/bin/chromium', 4 | PORTS: { 5 | testplane: { 6 | server: 8083, 7 | gui: 8073 8 | }, 9 | 'testplane-eye': { 10 | server: 8081, 11 | gui: 8071 12 | }, 13 | 'testplane-gui': { 14 | server: 8082, 15 | gui: 8072 16 | }, 17 | 'testplane-tinder': { 18 | server: 8086, 19 | gui: 8076 20 | }, 21 | plugins: { 22 | server: 8084, 23 | gui: 8074 24 | }, 25 | analytics: { 26 | server: 8085, 27 | gui: 8075 28 | }, 29 | 'db-migrations': { 30 | server: 8086, 31 | gui: 8076 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /test/func/utils/get-port.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {PORTS} = require('./constants'); 4 | 5 | const projectName = process.argv[2]; 6 | const context = process.argv[3]; 7 | 8 | process.stdout.write(PORTS[projectName][context]?.toString()); 9 | -------------------------------------------------------------------------------- /test/func/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.mkNestedSelector = (...args) => args.join(' '); 4 | -------------------------------------------------------------------------------- /test/setup/assert-ext.js: -------------------------------------------------------------------------------- 1 | global.assert.calledOnceWith = (...args) => { 2 | assert.calledOnce(args[0]); 3 | assert.calledWith(...args); 4 | }; 5 | 6 | global.assert.calledOnceWithExactly = function() { 7 | assert.calledOnce(arguments[0]); 8 | assert.calledWithExactly.apply(null, arguments); 9 | }; 10 | -------------------------------------------------------------------------------- /test/setup/configure-testing-library.js: -------------------------------------------------------------------------------- 1 | import {cleanup, configure} from '@testing-library/react'; 2 | 3 | export const mochaHooks = { 4 | beforeAll() { 5 | configure({testIdAttribute: 'data-qa'}); 6 | }, 7 | afterEach() { 8 | cleanup(); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/setup/css-modules-mock.js: -------------------------------------------------------------------------------- 1 | // Mock CSS modules by returning class name, e.g. styles['some-class'] resolves to some-class. 2 | // __esModule: false is needed due to the way SWC resolves modules at runtime. Becomes apparent at SWC playground. 3 | export const cssModulesMock = new Proxy({}, { 4 | get: (target, prop) => { 5 | if (prop === '__esModule') { 6 | return false; 7 | } 8 | 9 | return typeof prop === 'string' ? prop : ''; 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /test/setup/jsdom.js: -------------------------------------------------------------------------------- 1 | require('jsdom-global')(``, { 2 | url: 'http://localhost', 3 | pretendToBeVisual: true 4 | }); 5 | -------------------------------------------------------------------------------- /test/setup/ts-node.js: -------------------------------------------------------------------------------- 1 | require('ts-node').register({ 2 | swc: true, 3 | compilerOptions: { 4 | jsx: 'react' 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../tsconfig.common.json"], 3 | "exclude": ["./lib/static"], 4 | "include": ["./types.ts", "./lib"], 5 | "compilerOptions": { 6 | "noEmit": true, 7 | "module": "CommonJS", 8 | "paths": { 9 | "lib/*": ["../lib/*"], 10 | "@/*": ["../lib/*"] 11 | } 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import type sinon from 'sinon'; 2 | import {assert as chaiAssert} from 'chai'; 3 | 4 | declare global { 5 | const assert: typeof chaiAssert & sinon.SinonAssert & { 6 | calledOnceWith(spyOrSpyCall: sinon.SinonSpy | sinon.SinonSpyCall, ...args: TArgs): void; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/lib/plugin-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {getSuitePath} = require('lib/plugin-utils'); 4 | 5 | describe('getSuitePath', () => { 6 | it('should return correct path for simple suite', () => { 7 | const suite = { 8 | parent: { 9 | root: true 10 | }, 11 | title: 'some-title' 12 | }; 13 | 14 | const suitePath = getSuitePath(suite); 15 | 16 | assert.deepEqual(suitePath, ['some-title']); 17 | }); 18 | 19 | it('should return correct path for nested suite', () => { 20 | const suite = { 21 | parent: { 22 | parent: { 23 | root: true 24 | }, 25 | title: 'root-title' 26 | }, 27 | title: 'some-title' 28 | }; 29 | 30 | const suitePath = getSuitePath(suite); 31 | 32 | assert.deepEqual(suitePath, ['root-title', 'some-title']); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/unit/lib/static/components/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'plugin:react/recommended', 5 | globals: { 6 | mount: false 7 | }, 8 | env: { 9 | browser: true 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/unit/lib/static/modules/reducers/bottom-progress-bar.js: -------------------------------------------------------------------------------- 1 | const reducer = require('lib/static/modules/reducers/bottom-progress-bar').default; 2 | const actionNames = require('lib/static/modules/action-names').default; 3 | 4 | describe('lib/static/modules/reducers/bottom-progress-bar', () => { 5 | it('should update "currentRootSuiteId"', () => { 6 | const action = {type: actionNames.UPDATE_BOTTOM_PROGRESS_BAR, payload: {currentRootSuiteId: 'some-id'}}; 7 | const newState = reducer({}, action); 8 | 9 | assert.deepEqual(newState, { 10 | progressBar: { 11 | currentRootSuiteId: 'some-id' 12 | } 13 | }); 14 | }); 15 | 16 | it('should return passed state if action has not been handled', () => { 17 | const action = {type: 'non-existing-action-name'}; 18 | const newState = reducer({some: 'data'}, action); 19 | 20 | assert.deepEqual(newState, { 21 | some: 'data' 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/unit/lib/static/modules/reducers/stats.ts: -------------------------------------------------------------------------------- 1 | import reducer from 'lib/static/modules/reducers/stats'; 2 | import actionNames from 'lib/static/modules/action-names'; 3 | import type {State} from 'lib/static/new-ui/types/store'; 4 | import type {InitStaticReportAction} from 'lib/static/modules/actions/lifecycle'; 5 | 6 | describe('lib/static/modules/reducers/stats', () => { 7 | describe(`"${actionNames.INIT_STATIC_REPORT}" action`, () => { 8 | it('should not fail if stats is empty', () => { 9 | const action = {type: actionNames.INIT_STATIC_REPORT, payload: {stats: null} as InitStaticReportAction['payload']}; 10 | 11 | const newState = reducer({} as State, action); 12 | 13 | assert.deepEqual(newState.stats, {all: {}, perBrowser: undefined} as State['stats']); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/lib/static/modules/web-vitals.js: -------------------------------------------------------------------------------- 1 | const webVitals = require('web-vitals'); 2 | const {measurePerformance} = require('lib/static/modules/web-vitals'); 3 | 4 | describe('WebVitals', () => { 5 | const sandbox = sinon.createSandbox(); 6 | 7 | beforeEach(() => { 8 | sandbox.stub(webVitals, 'getCLS'); 9 | sandbox.stub(webVitals, 'getFID'); 10 | sandbox.stub(webVitals, 'getFCP'); 11 | sandbox.stub(webVitals, 'getLCP'); 12 | sandbox.stub(webVitals, 'getTTFB'); 13 | }); 14 | 15 | afterEach(() => sandbox.restore()); 16 | 17 | describe('measurePerformance', () => { 18 | ['getCLS', 'getFID', 'getFCP', 'getLCP', 'getTTFB'].forEach((methodName) => { 19 | it(`should call "${methodName}" if callback is passed`, () => { 20 | const spy = sinon.spy(); 21 | 22 | measurePerformance(spy); 23 | 24 | assert.calledOnceWith(webVitals[methodName], spy); 25 | }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/unit/lib/static/new-ui/components/Screenshot/index.tsx: -------------------------------------------------------------------------------- 1 | import {render} from '@testing-library/react'; 2 | import React from 'react'; 3 | import {Screenshot} from '@/static/new-ui/components/Screenshot'; 4 | 5 | describe('"FullScreenshot" component', () => { 6 | it('should encode symbols in path', () => { 7 | const screenshotComponent = render(); 8 | 9 | const image = screenshotComponent.getByRole('img') as HTMLImageElement; 10 | 11 | assert.include(image.src, 'images/%24/path'); 12 | }); 13 | 14 | it('should replace backslashes with slashes for screenshots', () => { 15 | const screenshotComponent = render(); 16 | 17 | const image = screenshotComponent.getByRole('img') as HTMLImageElement; 18 | 19 | assert.include(image.src, 'images/path'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/lib/static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../../tsconfig.common.json"], 3 | "include": ["../../../../lib/static/new-ui/types/typings.d.ts", "../../../types.ts", "."], 4 | "compilerOptions": { 5 | "jsx": "react", 6 | "allowJs": true, 7 | "module": "CommonJS", 8 | "noEmit": true, 9 | "paths": { 10 | "lib/*": ["../../../../lib/*"], 11 | "@/*": ["../../../../lib/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.common.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "es2023"], 4 | "allowJs": true, 5 | "declaration": true, 6 | "declarationMap": false, 7 | "esModuleInterop": true, 8 | "module": "commonjs", 9 | "noEmitOnError": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "skipLibCheck": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "target": "es2021", 18 | "resolveJsonModule": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.common.json", 3 | "include": ["lib", "testplane.ts", "hermione.ts", "gemini.js", "playwright.ts", "jest.ts"], 4 | "exclude": ["lib/static", "lib/bundle"], 5 | "compilerOptions": { 6 | "outDir": "build", 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const merge = require('webpack-merge'); 5 | 6 | const commonConfig = require('./webpack.common'); 7 | 8 | module.exports = merge( 9 | commonConfig, 10 | { 11 | mode: 'development', 12 | optimization: { 13 | minimize: false 14 | }, 15 | devtool: 'eval-source-map', 16 | plugins: [ 17 | new webpack.HotModuleReplacementPlugin() 18 | ] 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const merge = require('webpack-merge'); 5 | 6 | const commonConfig = require('./webpack.common'); 7 | 8 | module.exports = merge( 9 | commonConfig, 10 | { 11 | mode: 'production', 12 | optimization: { 13 | minimize: true 14 | }, 15 | plugins: [ 16 | new webpack.EnvironmentPlugin(['NODE_ENV']) 17 | ] 18 | } 19 | ); 20 | --------------------------------------------------------------------------------