├── .circleci └── config.yml ├── .env.sample ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Dockerfile ├── LICENSE ├── README.md ├── commitlint.config.js ├── images ├── overview.png ├── scoobyLogo.png └── status.png ├── lerna.json ├── package.json ├── packages ├── scooby-api │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── options.ts │ │ ├── s3 │ │ │ ├── api.ts │ │ │ ├── awsConfig.ts │ │ │ ├── index.ts │ │ │ └── resources.ts │ │ ├── types.ts │ │ └── utils │ │ │ └── clone.ts │ ├── test │ │ └── s3 │ │ │ └── resources.test.ts │ └── tsconfig.json ├── scooby-cli │ ├── .eslintignore │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ │ ├── run │ │ └── run.cmd │ ├── package.json │ ├── src │ │ ├── commands │ │ │ ├── fidelity-regression │ │ │ │ └── index.ts │ │ │ ├── fidelity │ │ │ │ └── index.ts │ │ │ ├── regression │ │ │ │ └── index.ts │ │ │ └── update-status │ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── shared │ │ │ ├── convert.ts │ │ │ └── flags.ts │ └── tsconfig.json ├── scooby-core │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── archive │ │ │ └── index.ts │ │ ├── comparison │ │ │ ├── batch.ts │ │ │ ├── code │ │ │ │ ├── diff │ │ │ │ │ ├── index.ts │ │ │ │ │ └── unix-diff.ts │ │ │ │ └── index.ts │ │ │ ├── image │ │ │ │ ├── diff │ │ │ │ │ ├── diffing.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normalization.ts │ │ │ │ │ └── overlap.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── worker.ts │ │ ├── environment │ │ │ ├── git.ts │ │ │ ├── index.ts │ │ │ ├── repositoryName.ts │ │ │ └── repositoryOwner.ts │ │ ├── index.ts │ │ ├── loading │ │ │ ├── index.ts │ │ │ ├── loader │ │ │ │ ├── index.ts │ │ │ │ └── util.ts │ │ │ └── options.ts │ │ ├── matching │ │ │ ├── default.ts │ │ │ ├── flexible.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ └── validation.ts │ │ ├── reports │ │ │ ├── fidelity-regression │ │ │ │ ├── index.ts │ │ │ │ ├── print.ts │ │ │ │ └── report │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── items.ts │ │ │ │ │ ├── results │ │ │ │ │ ├── code.ts │ │ │ │ │ ├── image.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── util.ts │ │ │ │ │ └── summary.ts │ │ │ ├── fidelity │ │ │ │ ├── index.ts │ │ │ │ ├── print.ts │ │ │ │ ├── report.ts │ │ │ │ └── threshold.ts │ │ │ ├── index.ts │ │ │ ├── name.ts │ │ │ ├── output │ │ │ │ ├── hosted.ts │ │ │ │ ├── index.ts │ │ │ │ └── zip.ts │ │ │ ├── regression │ │ │ │ ├── index.ts │ │ │ │ ├── print.ts │ │ │ │ └── report │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── items.ts │ │ │ │ │ ├── results │ │ │ │ │ ├── code.ts │ │ │ │ │ ├── image.ts │ │ │ │ │ └── index.ts │ │ │ │ │ └── summary.ts │ │ │ └── shared │ │ │ │ ├── match-validation.ts │ │ │ │ ├── reference.ts │ │ │ │ ├── regression.ts │ │ │ │ └── snapshot.ts │ │ ├── review │ │ │ └── index.ts │ │ ├── source │ │ │ ├── code │ │ │ │ ├── batch.ts │ │ │ │ ├── index.ts │ │ │ │ └── worker.ts │ │ │ ├── image │ │ │ │ ├── html │ │ │ │ │ ├── index.ts │ │ │ │ │ └── runtime.ts │ │ │ │ ├── index.ts │ │ │ │ └── png.ts │ │ │ └── index.ts │ │ ├── status │ │ │ ├── compute.ts │ │ │ ├── index.ts │ │ │ ├── reports.ts │ │ │ ├── review.ts │ │ │ └── url.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── clone.ts │ │ │ ├── commit.ts │ │ │ ├── env.ts │ │ │ ├── hash.ts │ │ │ ├── resource.ts │ │ │ └── temp.ts │ ├── test │ │ ├── data │ │ │ ├── fidelity-regression │ │ │ │ ├── html-bad-fidelity-no-regression │ │ │ │ │ ├── actual │ │ │ │ │ │ ├── test1.html │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test3.html │ │ │ │ │ ├── expected │ │ │ │ │ │ ├── test1.html │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test3.html │ │ │ │ │ └── reference │ │ │ │ │ │ ├── test1.html │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test3.html │ │ │ │ ├── html-bad-fidelity-with-regression │ │ │ │ │ ├── actual │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test4.html │ │ │ │ │ ├── expected │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test4.html │ │ │ │ │ └── reference │ │ │ │ │ │ ├── test1.html │ │ │ │ │ │ └── test2.html │ │ │ │ ├── json-bad-fidelity-no-matching │ │ │ │ │ ├── actual │ │ │ │ │ │ └── test3.json │ │ │ │ │ ├── expected │ │ │ │ │ │ └── test3.json │ │ │ │ │ └── reference │ │ │ │ │ │ ├── test1.json │ │ │ │ │ │ └── test2.json │ │ │ │ └── json-bad-fidelity-with-regression │ │ │ │ │ ├── actual │ │ │ │ │ ├── test1.json │ │ │ │ │ ├── test2.json │ │ │ │ │ └── test4.json │ │ │ │ │ ├── expected │ │ │ │ │ ├── test1.json │ │ │ │ │ ├── test2.json │ │ │ │ │ └── test4.json │ │ │ │ │ └── reference │ │ │ │ │ ├── test1.json │ │ │ │ │ ├── test2.json │ │ │ │ │ └── test3.json │ │ │ ├── fidelity │ │ │ │ ├── html-perfect-fidelity │ │ │ │ │ ├── actual │ │ │ │ │ │ ├── test1.html │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test3.html │ │ │ │ │ └── expected │ │ │ │ │ │ ├── test1.html │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test3.html │ │ │ │ ├── html-with-differences │ │ │ │ │ ├── actual │ │ │ │ │ │ ├── test1.html │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test3.html │ │ │ │ │ └── expected │ │ │ │ │ │ ├── test1.html │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test3.html │ │ │ │ ├── json-perfect-fidelity │ │ │ │ │ ├── actual │ │ │ │ │ │ ├── test1.json │ │ │ │ │ │ └── test2.json │ │ │ │ │ └── expected │ │ │ │ │ │ ├── test1.json │ │ │ │ │ │ └── test2.json │ │ │ │ ├── json-with-differences │ │ │ │ │ ├── actual │ │ │ │ │ │ ├── test1.json │ │ │ │ │ │ └── test2.json │ │ │ │ │ └── expected │ │ │ │ │ │ ├── test1.json │ │ │ │ │ │ └── test2.json │ │ │ │ ├── jsx-perfect-fidelity │ │ │ │ │ ├── actual │ │ │ │ │ │ ├── test1.jsx │ │ │ │ │ │ └── test2.jsx │ │ │ │ │ └── expected │ │ │ │ │ │ ├── test1.jsx │ │ │ │ │ │ └── test2.jsx │ │ │ │ ├── jsx-with-differences │ │ │ │ │ ├── actual │ │ │ │ │ │ ├── test1.jsx │ │ │ │ │ │ └── test2.jsx │ │ │ │ │ └── expected │ │ │ │ │ │ ├── test1.jsx │ │ │ │ │ │ └── test2.jsx │ │ │ │ ├── non-matching │ │ │ │ │ ├── actual │ │ │ │ │ │ ├── test1.html │ │ │ │ │ │ └── test2.html │ │ │ │ │ └── expected │ │ │ │ │ │ ├── test2.html │ │ │ │ │ │ └── test3.html │ │ │ │ └── unformatted-equal-json │ │ │ │ │ ├── actual │ │ │ │ │ ├── test1.json │ │ │ │ │ └── test2.json │ │ │ │ │ └── expected │ │ │ │ │ ├── test1.json │ │ │ │ │ └── test2.json │ │ │ ├── loading │ │ │ │ ├── basic-metadata │ │ │ │ │ ├── test1-image.scooby.png │ │ │ │ │ ├── test1.code.scooby.jsx │ │ │ │ │ ├── test1.html │ │ │ │ │ ├── test1.scooby.json │ │ │ │ │ ├── test2-code.scooby.html │ │ │ │ │ ├── test2.html │ │ │ │ │ └── test2.scooby.json │ │ │ │ ├── basic-test-structure │ │ │ │ │ ├── test1.html │ │ │ │ │ ├── test1.scooby.json │ │ │ │ │ └── test2.html │ │ │ │ ├── invalid-metadata-path │ │ │ │ │ ├── test1.html │ │ │ │ │ └── test1.scooby.json │ │ │ │ ├── multiple-file-types │ │ │ │ │ ├── test1.css │ │ │ │ │ ├── test1.html │ │ │ │ │ └── test2.html │ │ │ │ ├── nested-metadata │ │ │ │ │ ├── assets.scooby │ │ │ │ │ │ └── code.js │ │ │ │ │ └── test │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── scooby.json │ │ │ │ ├── nested-multiple-files-test-structure │ │ │ │ │ ├── test1 │ │ │ │ │ │ ├── another.html │ │ │ │ │ │ ├── another.scooby.json │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── index.scooby.json │ │ │ │ │ ├── test2 │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── scooby.json │ │ │ │ │ └── test3 │ │ │ │ │ │ └── file.html │ │ │ │ ├── nested-test-structure │ │ │ │ │ ├── test1 │ │ │ │ │ │ └── index.html │ │ │ │ │ └── test2 │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── scooby.json │ │ │ │ └── skip-scooby-entries-correctly │ │ │ │ │ ├── test1 │ │ │ │ │ ├── index.html │ │ │ │ │ ├── to-skip.scooby.html │ │ │ │ │ └── toskip.scooby │ │ │ │ │ │ └── index.html │ │ │ │ │ └── test2 │ │ │ │ │ ├── index.html │ │ │ │ │ ├── index.scooby.html │ │ │ │ │ └── scooby.json │ │ │ └── regression │ │ │ │ ├── html-no-matching │ │ │ │ ├── actual │ │ │ │ │ └── test3.html │ │ │ │ └── expected │ │ │ │ │ ├── test1.html │ │ │ │ │ └── test2.html │ │ │ │ ├── html-no-regressions │ │ │ │ ├── actual │ │ │ │ │ ├── test1.html │ │ │ │ │ └── test2.html │ │ │ │ └── expected │ │ │ │ │ ├── test1.html │ │ │ │ │ └── test2.html │ │ │ │ ├── html-regression-with-differences │ │ │ │ ├── actual │ │ │ │ │ ├── test1.html │ │ │ │ │ ├── test2.html │ │ │ │ │ └── test4.html │ │ │ │ └── expected │ │ │ │ │ ├── test1.html │ │ │ │ │ ├── test2.html │ │ │ │ │ └── test3.html │ │ │ │ ├── json-no-matching │ │ │ │ ├── actual │ │ │ │ │ └── test3.json │ │ │ │ └── expected │ │ │ │ │ ├── test1.json │ │ │ │ │ └── test2.json │ │ │ │ ├── json-no-regression-if-formatted │ │ │ │ ├── actual │ │ │ │ │ ├── test1.json │ │ │ │ │ └── test2.json │ │ │ │ └── expected │ │ │ │ │ ├── test1.json │ │ │ │ │ └── test2.json │ │ │ │ ├── json-no-regression │ │ │ │ ├── actual │ │ │ │ │ ├── test1.json │ │ │ │ │ └── test2.json │ │ │ │ └── expected │ │ │ │ │ ├── test1.json │ │ │ │ │ └── test2.json │ │ │ │ ├── json-with-regression │ │ │ │ ├── actual │ │ │ │ │ ├── test1.json │ │ │ │ │ ├── test2.json │ │ │ │ │ └── test4.json │ │ │ │ └── expected │ │ │ │ │ ├── test1.json │ │ │ │ │ ├── test2.json │ │ │ │ │ └── test3.json │ │ │ │ ├── jsx-no-regression │ │ │ │ ├── actual │ │ │ │ │ ├── test1.jsx │ │ │ │ │ └── test2.jsx │ │ │ │ └── expected │ │ │ │ │ ├── test1.jsx │ │ │ │ │ └── test2.jsx │ │ │ │ └── jsx-with-regression │ │ │ │ ├── actual │ │ │ │ ├── test1.jsx │ │ │ │ └── test2.jsx │ │ │ │ └── expected │ │ │ │ ├── test1.jsx │ │ │ │ └── test2.jsx │ │ ├── loading.test.ts │ │ ├── matching.test.ts │ │ ├── name.test.ts │ │ ├── reports │ │ │ ├── fidelity-regression.test.ts │ │ │ ├── fidelity.test.ts │ │ │ └── regression.test.ts │ │ └── status │ │ │ ├── compute.test.ts │ │ │ └── review.test.ts │ └── tsconfig.json ├── scooby-github-api │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── index.ts │ │ │ └── octokit.ts │ │ ├── index.ts │ │ ├── options.ts │ │ └── types.ts │ └── tsconfig.json ├── scooby-service │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── index.ts │ │ │ └── review.ts │ │ ├── auth.ts │ │ ├── context.ts │ │ ├── env.ts │ │ └── index.ts │ └── tsconfig.json ├── scooby-shared │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── parsing.ts │ │ ├── s3.ts │ │ └── types.ts │ └── tsconfig.json └── scooby-web │ ├── .parcelrc │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── scripts │ └── prepare-env-variables.js │ ├── src │ ├── assets │ │ ├── scoobyLogo.png │ │ └── scoobyLogo.svg │ ├── components │ │ ├── CodeComparator │ │ │ ├── CodeComparator.tsx │ │ │ ├── CodeComparatorController.tsx │ │ │ ├── index.ts │ │ │ └── useSource.ts │ │ ├── EnhancedLink.tsx │ │ ├── EntryList │ │ │ ├── EntryList.tsx │ │ │ ├── index.ts │ │ │ └── modes │ │ │ │ ├── badge.tsx │ │ │ │ ├── basic │ │ │ │ ├── ListItem.tsx │ │ │ │ └── index.tsx │ │ │ │ ├── file-tree │ │ │ │ ├── index.tsx │ │ │ │ ├── tree.test.ts │ │ │ │ └── tree.tsx │ │ │ │ ├── image │ │ │ │ ├── ListItem.tsx │ │ │ │ └── index.tsx │ │ │ │ └── index.ts │ │ ├── ErrorPanel.tsx │ │ ├── ImageComparator │ │ │ ├── ImageComparator.tsx │ │ │ ├── ImageComparatorController.tsx │ │ │ └── index.ts │ │ ├── Loader │ │ │ ├── index.tsx │ │ │ └── loader.css │ │ ├── MetadataList │ │ │ ├── ListItem │ │ │ │ ├── BaseListItem.tsx │ │ │ │ ├── CodeListItem.tsx │ │ │ │ ├── FileListItem.tsx │ │ │ │ ├── ImageListItem.tsx │ │ │ │ ├── LinkListItem.tsx │ │ │ │ ├── TextListItem.tsx │ │ │ │ └── index.tsx │ │ │ ├── MetadataList.tsx │ │ │ └── index.ts │ │ ├── SplitPane.tsx │ │ ├── StatsView │ │ │ ├── FractionStatisticView.tsx │ │ │ ├── GaugeStatisticView.tsx │ │ │ ├── StatisticView.tsx │ │ │ ├── StatsView.tsx │ │ │ └── index.ts │ │ └── SummaryBadge.tsx │ ├── data-fetching │ │ ├── api │ │ │ ├── index.ts │ │ │ ├── mixed.ts │ │ │ ├── provider.tsx │ │ │ ├── rest │ │ │ │ ├── config.ts │ │ │ │ └── index.ts │ │ │ ├── s3 │ │ │ │ ├── config.ts │ │ │ │ └── index.ts │ │ │ ├── types.ts │ │ │ └── zip.ts │ │ └── hooks │ │ │ ├── internal │ │ │ ├── mapping.ts │ │ │ └── useQuery.ts │ │ │ ├── useAggregateReview.ts │ │ │ ├── useCommitStatusOverview.ts │ │ │ ├── useReport.ts │ │ │ ├── useReportStatus.ts │ │ │ ├── useReports.ts │ │ │ ├── useURL.ts │ │ │ └── useZipArchiveLoader.ts │ ├── images.d.ts │ ├── index.css │ ├── index.html │ ├── index.tsx │ ├── providers │ │ └── feedback │ │ │ └── index.tsx │ ├── router.tsx │ ├── routes │ │ ├── ErrorPage.tsx │ │ ├── Home.tsx │ │ ├── Root.tsx │ │ ├── commit │ │ │ ├── Commit.tsx │ │ │ ├── CommitController.tsx │ │ │ ├── ReportList │ │ │ │ ├── ReportListItem.tsx │ │ │ │ ├── ReportListItemController.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── params.ts │ │ │ ├── useEnhancedNavigation.ts │ │ │ ├── useQueryParams.ts │ │ │ └── useUpdateParams.ts │ │ ├── report │ │ │ ├── ApproveButton │ │ │ │ ├── ApproveButton.tsx │ │ │ │ ├── ApproveButtonController.tsx │ │ │ │ └── index.ts │ │ │ ├── Report.tsx │ │ │ ├── ReportController.tsx │ │ │ ├── fidelity-regression │ │ │ │ ├── FidelityRegressionReport.tsx │ │ │ │ ├── actions.ts │ │ │ │ ├── comparison │ │ │ │ │ ├── ImageComparator │ │ │ │ │ │ ├── ImageComparator.tsx │ │ │ │ │ │ ├── ImageComparatorController.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── ImageComparisonView.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── details │ │ │ │ │ ├── MetadataTab.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.css │ │ │ │ ├── entries.ts │ │ │ │ └── index.tsx │ │ │ ├── fidelity │ │ │ │ ├── FidelityReport.tsx │ │ │ │ ├── actions.ts │ │ │ │ ├── comparison │ │ │ │ │ ├── CodeComparisonView.tsx │ │ │ │ │ ├── ImageComparisonView.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── details │ │ │ │ │ ├── MetadataTab.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.css │ │ │ │ ├── entries.ts │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── regression │ │ │ │ ├── RegressionReport.tsx │ │ │ │ ├── actions.ts │ │ │ │ ├── comparison │ │ │ │ ├── CodeComparisonView.tsx │ │ │ │ ├── ImageComparisonView.tsx │ │ │ │ └── index.tsx │ │ │ │ ├── details │ │ │ │ ├── MetadataTab.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.css │ │ │ │ ├── entries.ts │ │ │ │ └── index.tsx │ │ └── useDropzone.tsx │ ├── static │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ ├── safari-pinned-tab.svg │ │ └── site.webmanifest │ ├── types.ts │ └── utils │ │ ├── capitalize.ts │ │ ├── colors.ts │ │ ├── language.ts │ │ ├── rank.ts │ │ ├── review.ts │ │ └── search.ts │ └── tsconfig.json ├── sample-projects ├── basic-fidelity │ ├── actual │ │ ├── test1.html │ │ ├── test2.html │ │ └── test3.html │ └── expected │ │ ├── test1.html │ │ ├── test2.html │ │ └── test3.html ├── basic-regression │ ├── actual │ │ ├── test1.html │ │ ├── test2.html │ │ └── test3.html │ └── expected │ │ ├── test1.html │ │ ├── test2.html │ │ └── this-has-been-removed.html ├── ci-html-code-regression │ ├── test1.css │ ├── test1.html │ ├── test2.css │ ├── test2.html │ ├── test3.css │ └── test3.html ├── ci-html-fidelity-metadata │ ├── actual │ │ ├── test1-image.scooby.png │ │ ├── test1.code.scooby.jsx │ │ ├── test1.html │ │ ├── test1.scooby.json │ │ ├── test2.html │ │ ├── test2.scooby.json │ │ └── test3.html │ └── expected │ │ ├── test1.html │ │ ├── test2.html │ │ ├── test2.scooby.json │ │ └── test3.html ├── ci-html-fidelity-regression-shared-images │ ├── actual │ │ ├── 1234-html │ │ │ └── test1.html │ │ └── 1234-react │ │ │ └── test1.html │ └── expected │ │ └── 1234 │ │ └── Test1.png ├── ci-html-fidelity-regression │ ├── actual │ │ ├── test1.html │ │ ├── test2.html │ │ └── test3.html │ ├── expected │ │ ├── test1.html │ │ ├── test2.html │ │ └── test3.html │ └── reference │ │ ├── test1.html │ │ ├── test2.html │ │ └── test3.html ├── ci-html-fidelity │ ├── actual │ │ ├── test1.html │ │ ├── test2.html │ │ └── test3.html │ └── expected │ │ ├── test1.html │ │ ├── test2.html │ │ └── test3.html ├── ci-html-regression │ ├── test1.html │ ├── test2.html │ └── test3.html ├── ci-json-fidelity │ ├── actual │ │ ├── test1.json │ │ └── test2.json │ └── expected │ │ ├── test1.json │ │ └── test2.json ├── ci-json-regression-metadata │ ├── test1-image.scooby.png │ ├── test1.code.scooby.jsx │ ├── test1.json │ ├── test1.scooby.json │ ├── test2.json │ └── test2.scooby.json ├── ci-json-regression │ ├── test1.json │ └── test2.json ├── ci-jsx-fidelity-mixed │ ├── actual │ │ ├── test1.css │ │ ├── test1.jsx │ │ └── test2.jsx │ └── expected │ │ ├── test1.css │ │ ├── test1.jsx │ │ └── test2.jsx ├── ci-jsx-fidelity │ ├── actual │ │ ├── test1.jsx │ │ └── test2.jsx │ └── expected │ │ ├── test1.jsx │ │ └── test2.jsx ├── ci-jsx-regression-mixed │ ├── test1.css │ ├── test1.jsx │ └── test2.jsx ├── ci-jsx-regression │ ├── test1.jsx │ └── test2.jsx ├── ci-mixed-fidelity │ ├── actual │ │ ├── test1.html │ │ ├── test2.html │ │ └── test3.html │ └── expected │ │ ├── test1.png │ │ ├── test2.png │ │ └── test3.png ├── ci-nested-jsx-fidelity │ ├── actual │ │ ├── project1 │ │ │ ├── test1.jsx │ │ │ └── test2.jsx │ │ └── project2 │ │ │ └── test1.jsx │ └── expected │ │ ├── project1 │ │ ├── test1.jsx │ │ └── test2.jsx │ │ └── project2 │ │ └── test1.jsx ├── ci-png-regression │ ├── form.png │ ├── menu-horizontal.png │ └── menu-vertical.png └── ci-recursive-regression │ ├── html │ ├── frame-1.html │ └── img │ │ ├── ellipse-2-1@2x.png │ │ ├── ellipse-2-2@2x.png │ │ ├── rectangle-4-1@2x.png │ │ ├── rectangle-4-2@2x.png │ │ ├── rectangle-7-1@2x.png │ │ └── rectangle-7-2@2x.png │ ├── react │ ├── img │ │ ├── ellipse-2-1@2x.png │ │ ├── ellipse-2-2@2x.png │ │ ├── rectangle-4-1@2x.png │ │ ├── rectangle-4-2@2x.png │ │ ├── rectangle-7-1@2x.png │ │ └── rectangle-7-2@2x.png │ ├── index.c50a4e06.js │ ├── index.c50a4e06.js.map │ ├── index.f8336f1e.css │ ├── index.f8336f1e.css.map │ └── index.html │ └── vue │ ├── img │ ├── ellipse-2-1@2x.png │ ├── ellipse-2-2@2x.png │ ├── rectangle-4-1@2x.png │ ├── rectangle-4-2@2x.png │ ├── rectangle-7-1@2x.png │ └── rectangle-7-2@2x.png │ ├── index.html │ └── js │ └── app.js ├── scripts ├── patch_pending_circleci_approval.sh ├── publish_to_public_npm_registry.sh ├── remove_unnecessary_packages.sh └── run_all_tests.sh └── yarn.lock /.env.sample: -------------------------------------------------------------------------------- 1 | SCOOBY_AWS_ACCESS_KEY_ID= 2 | SCOOBY_AWS_SECRET_ACCESS_KEY= 3 | SCOOBY_AWS_S3_BUCKET= 4 | SCOOBY_AWS_S3_REGION= 5 | SCOOBY_GITHUB_ACCESS_TOKEN= 6 | SCOOBY_WEB_BASE_URL=http://localhost:1234 7 | SCOOBY_REPOSITORY_OWNER=AnimaApp 8 | SCOOBY_SERVICE_ACCESS_TOKEN=local-testing 9 | SCOOBY_SERVICE_BASE_URL=https://scooby-api.animaapp.com 10 | DOPPLER_TOKEN= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": ["@typescript-eslint"], 18 | "rules": { 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "@typescript-eslint/no-explicit-any": "off" 21 | } 22 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "[typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build-cli", 6 | "command": "yarn", 7 | "args": ["workspace", "@animaapp/scooby-cli", "build"], 8 | "type": "shell" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Anima App, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } -------------------------------------------------------------------------------- /images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/images/overview.png -------------------------------------------------------------------------------- /images/scoobyLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/images/scoobyLogo.png -------------------------------------------------------------------------------- /images/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/images/status.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "yarn", 6 | "version": "independent", 7 | "command": { 8 | "publish": { 9 | "message": "chore(release): publish", 10 | "registry": "https://npm.pkg.github.com" 11 | } 12 | }, 13 | "useWorkspaces": true 14 | } -------------------------------------------------------------------------------- /packages/scooby-api/README.md: -------------------------------------------------------------------------------- 1 | # Scooby 2 | 3 | This package is part of the [Scooby](https://github.com/AnimaApp/scooby) project, 4 | an optimized regression and fidelity testing framework. 5 | 6 | For more information, please visit the [documentation](https://github.com/AnimaApp/scooby). 7 | -------------------------------------------------------------------------------- /packages/scooby-api/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testPathIgnorePatterns: ["/node_modules/", ".*shared.test.*"], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/scooby-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animaapp/scooby-api", 3 | "version": "1.27.0", 4 | "description": "This package contains the logic to upload/retrieve test results", 5 | "main": "dist/src/index.js", 6 | "repository": "https://github.com/AnimaApp/scooby", 7 | "publishConfig": { 8 | "registry": "https://npm.pkg.github.com" 9 | }, 10 | "license": "MIT", 11 | "scripts": { 12 | "build": "tsc --build", 13 | "clean": "rm -Rf ./dist", 14 | "test": "jest --passWithNoTests" 15 | }, 16 | "devDependencies": { 17 | "@types/jest": "^27.4.1", 18 | "@types/node": "^18.8.2", 19 | "@types/uuid": "^8.3.4", 20 | "jest": "^29.2.1", 21 | "ts-jest": "^29.0.3" 22 | }, 23 | "dependencies": { 24 | "@animaapp/scooby-shared": "^1.27.0", 25 | "@aws-sdk/client-s3": "3.186.0", 26 | "fastq": "^1.13.0", 27 | "uuid": "^9.0.0", 28 | "zod": "^3.19.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/scooby-api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { getS3ScoobyAPI } from "./s3"; 2 | import { ScoobyAPI } from "./types"; 3 | import { ScoobyAPIOptions, validateOptions } from "./options"; 4 | 5 | export async function getScoobyAPI( 6 | options?: Partial 7 | ): Promise { 8 | const effectiveOptions = validateOptions(options); 9 | 10 | if (effectiveOptions.provider === "s3") { 11 | return getS3ScoobyAPI(effectiveOptions); 12 | } 13 | 14 | throw new Error("could not initialize API, invalid configuration detected"); 15 | } 16 | 17 | export type { ScoobyAPI, CommitContext, ReportContext } from "./types"; 18 | -------------------------------------------------------------------------------- /packages/scooby-api/src/options.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const optionsSchema = z.object({ 4 | provider: z.enum(["s3"]), 5 | repositoryName: z.string(), 6 | awsCredentials: z.optional( 7 | z.object({ 8 | accessKeyId: z.string(), 9 | secretAccessKey: z.string(), 10 | }) 11 | ), 12 | awsS3Bucket: z.optional( 13 | z.object({ 14 | name: z.string(), 15 | region: z.string(), 16 | }) 17 | ), 18 | maxConcurrentUploads: z.number().int(), 19 | }); 20 | 21 | export type ScoobyAPIOptions = z.infer; 22 | 23 | const DEFAULT_OPTIONS: Partial = { 24 | provider: "s3", 25 | maxConcurrentUploads: 8, 26 | }; 27 | 28 | export function validateOptions( 29 | options: Partial | undefined 30 | ): ScoobyAPIOptions { 31 | const validatedOptions = optionsSchema.parse({ 32 | ...DEFAULT_OPTIONS, 33 | ...options, 34 | }); 35 | 36 | return validatedOptions; 37 | } 38 | -------------------------------------------------------------------------------- /packages/scooby-api/src/s3/index.ts: -------------------------------------------------------------------------------- 1 | import { ScoobyAPIOptions } from "../options"; 2 | import { ScoobyAPI } from "../types"; 3 | import { S3ScoobyAPI } from "./api"; 4 | 5 | export async function getS3ScoobyAPI( 6 | options: ScoobyAPIOptions 7 | ): Promise { 8 | return new S3ScoobyAPI(options); 9 | } 10 | -------------------------------------------------------------------------------- /packages/scooby-api/src/utils/clone.ts: -------------------------------------------------------------------------------- 1 | export function clone(obj: T): T { 2 | return JSON.parse(JSON.stringify(obj)); 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react", 5 | "moduleResolution": "node", 6 | "module": "CommonJS", 7 | "resolveJsonModule": true, 8 | "outDir": "./dist", 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "composite": true, 12 | "strict": true, 13 | "lib": [ 14 | "es2019" 15 | ], 16 | "allowSyntheticDefaultImports": true, 17 | "skipLibCheck": true, 18 | "paths": { 19 | "@animaapp/scooby-shared": ["../scooby-shared"] 20 | } 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | "__tests__", 25 | "dist", 26 | "test" 27 | ], 28 | "references": [ 29 | {"path": "../scooby-shared"} 30 | ] 31 | } -------------------------------------------------------------------------------- /packages/scooby-cli/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /packages/scooby-cli/.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /package-lock.json 7 | /tmp 8 | node_modules 9 | oclif.manifest.json 10 | -------------------------------------------------------------------------------- /packages/scooby-cli/README.md: -------------------------------------------------------------------------------- 1 | # Scooby 2 | 3 | This package is part of the [Scooby](https://github.com/AnimaApp/scooby) project, 4 | an optimized regression and fidelity testing framework. 5 | 6 | For more information, please visit the [documentation](https://github.com/AnimaApp/scooby). 7 | -------------------------------------------------------------------------------- /packages/scooby-cli/bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("dotenv").config(); 4 | 5 | const oclif = require("@oclif/core"); 6 | 7 | oclif 8 | .run() 9 | .then(require("@oclif/core/flush")) 10 | .catch((e) => { 11 | console.error(""); 12 | console.error("######################## FULL STACKTRACE #####################") 13 | console.error(e); 14 | console.error("##############################################################"); 15 | console.error(""); 16 | 17 | require("@oclif/errors/handle")(e) 18 | }); 19 | -------------------------------------------------------------------------------- /packages/scooby-cli/bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /packages/scooby-cli/src/commands/update-status/index.ts: -------------------------------------------------------------------------------- 1 | import { runUpdateStatus } from "@animaapp/scooby-core"; 2 | import { Command } from "@oclif/core"; 3 | 4 | export default class UpdateGitHubStatus extends Command { 5 | static description = 6 | "Update a PR/Commit status (including GitHub), based on the previously executed reports and reviews."; 7 | 8 | static examples = [`$ scooby update-status`]; 9 | 10 | static flags = {}; 11 | 12 | async run(): Promise { 13 | await this.parse(UpdateGitHubStatus); 14 | 15 | await runUpdateStatus(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/scooby-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from "@oclif/core"; 2 | -------------------------------------------------------------------------------- /packages/scooby-cli/src/shared/convert.ts: -------------------------------------------------------------------------------- 1 | import { ReportOutputTargetOrAuto } from "@animaapp/scooby-core"; 2 | import { OutputType } from "./flags"; 3 | 4 | export function convertFlagsToReportOutputTarget( 5 | reportName: string, 6 | flags: { output: OutputType } 7 | ): ReportOutputTargetOrAuto { 8 | switch (flags.output) { 9 | case "auto": 10 | return { 11 | type: "auto", 12 | }; 13 | case "zip": 14 | return { 15 | type: "zip", 16 | path: `${reportName}.zip`, 17 | }; 18 | case "hosted": 19 | return { 20 | type: "hosted", 21 | }; 22 | } 23 | } 24 | 25 | export function parseFileTypesFlag( 26 | fileTypes: string | undefined 27 | ): string[] | undefined { 28 | if (!fileTypes) { 29 | return; 30 | } 31 | 32 | return fileTypes.split(",").map((fileType) => fileType.trim()); 33 | } 34 | -------------------------------------------------------------------------------- /packages/scooby-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "strict": true, 9 | "target": "es2019", 10 | "esModuleInterop": true, 11 | "paths": { 12 | "@animaapp/scooby-core": ["../scooby-core"] 13 | } 14 | }, 15 | "include": ["src/**/*"], 16 | "references": [{ "path": "../scooby-core" }] 17 | } 18 | -------------------------------------------------------------------------------- /packages/scooby-core/README.md: -------------------------------------------------------------------------------- 1 | # Scooby 2 | 3 | This package is part of the [Scooby](https://github.com/AnimaApp/scooby) project, 4 | an optimized regression and fidelity testing framework. 5 | 6 | For more information, please visit the [documentation](https://github.com/AnimaApp/scooby). 7 | -------------------------------------------------------------------------------- /packages/scooby-core/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testPathIgnorePatterns: ["/node_modules/", ".*shared.test.*"], 6 | testTimeout: 120000, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/scooby-core/src/comparison/code/diff/index.ts: -------------------------------------------------------------------------------- 1 | import { UnixDiffComparator } from "./unix-diff"; 2 | 3 | export type CodeComparator = { 4 | getName: () => string; 5 | compare: ( 6 | expectedPath: string, 7 | actualPath: string 8 | ) => Promise; 9 | }; 10 | 11 | export type CodeComparisonResult = { 12 | similarity: number; 13 | totalLines: number; 14 | changedLines: number; 15 | differenceFilePath?: string; 16 | }; 17 | 18 | export type CodeComparisonOptions = { 19 | comparator?: string; 20 | }; 21 | 22 | const COMPARATORS = [new UnixDiffComparator()] as const; 23 | 24 | export function getCodeComparator( 25 | options?: CodeComparisonOptions 26 | ): CodeComparator { 27 | for (const comparator of COMPARATORS) { 28 | if (comparator.getName() === (options?.comparator ?? "unix-diff")) { 29 | return comparator; 30 | } 31 | } 32 | 33 | throw new Error( 34 | "could not find comparator satisfying the given configuration" 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/scooby-core/src/comparison/code/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BatchComparisonOptions, 3 | CodeBatchComparisonEntry, 4 | CodeBatchComparisonResult, 5 | CodeComparisonTaskRequest, 6 | CodeComparisonTaskResult, 7 | } from "../types"; 8 | import { CodeSourceEntry } from "../../types"; 9 | import { runComparisonBatch } from "../batch"; 10 | import { MatchedPair } from "../../matching"; 11 | 12 | export async function performBatchCodeComparison( 13 | pairs: MatchedPair[], 14 | options?: Partial 15 | ): Promise { 16 | const requests: CodeComparisonTaskRequest[] = pairs.map((pair) => ({ 17 | actual: pair.actual, 18 | expected: pair.expected, 19 | options: options?.codeComparisonOptions, 20 | type: "code", 21 | })); 22 | 23 | const comparisons = await runComparisonBatch< 24 | CodeComparisonTaskRequest, 25 | CodeComparisonTaskResult 26 | >(requests, options); 27 | 28 | return { 29 | type: "code", 30 | comparisons: comparisons.map(mapToComparisonEntry), 31 | }; 32 | } 33 | 34 | function mapToComparisonEntry( 35 | result: CodeComparisonTaskResult 36 | ): CodeBatchComparisonEntry { 37 | return { 38 | type: "code", 39 | actual: result.actual, 40 | comparison: result.comparison, 41 | expected: result.expected, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/scooby-core/src/comparison/image/diff/overlap.ts: -------------------------------------------------------------------------------- 1 | import sharp from "sharp"; 2 | import { createTemporaryFile } from "../../../utils/temp"; 3 | 4 | export type ImageOverlapResult = { 5 | overlapImagePath: string; 6 | }; 7 | 8 | export async function calculateImageOverlap( 9 | normalizedExpectedPath: string, 10 | normalizedActualPath: string 11 | ): Promise { 12 | const overlay = await sharp(normalizedActualPath) 13 | .composite([ 14 | { 15 | input: Buffer.from([255, 255, 255, 128]), 16 | raw: { 17 | width: 1, 18 | height: 1, 19 | channels: 4, 20 | }, 21 | tile: true, 22 | blend: "dest-in", 23 | }, 24 | ]) 25 | .toBuffer(); 26 | 27 | const overlapImagePath = await createTemporaryFile("overlap", ".png"); 28 | 29 | await sharp(normalizedExpectedPath) 30 | .composite([{ input: overlay, gravity: "northwest" }]) 31 | .toFile(overlapImagePath); 32 | 33 | return { 34 | overlapImagePath, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/scooby-core/src/comparison/image/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BatchComparisonOptions, 3 | ImageBatchComparisonEntry, 4 | ImageBatchComparisonResult, 5 | ImageComparisonTaskRequest, 6 | ImageComparisonTaskResult, 7 | } from "../types"; 8 | import { ImageSourceEntry } from "../../types"; 9 | import { runComparisonBatch } from "../batch"; 10 | import { MatchedPair } from "../../matching"; 11 | 12 | export async function performBatchImageComparison( 13 | pairs: MatchedPair[], 14 | options?: Partial 15 | ): Promise { 16 | const requests: ImageComparisonTaskRequest[] = pairs.map((pair) => ({ 17 | actual: pair.actual, 18 | expected: pair.expected, 19 | options: options?.imageComparisonOptions, 20 | type: "image", 21 | })); 22 | 23 | const comparisons = await runComparisonBatch< 24 | ImageComparisonTaskRequest, 25 | ImageComparisonTaskResult 26 | >(requests, options); 27 | 28 | return { 29 | type: "image", 30 | comparisons: comparisons.map(mapToComparisonEntry), 31 | }; 32 | } 33 | 34 | function mapToComparisonEntry( 35 | result: ImageComparisonTaskResult 36 | ): ImageBatchComparisonEntry { 37 | return { 38 | type: "image", 39 | actual: result.actual, 40 | comparison: result.comparison, 41 | expected: result.expected, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/scooby-core/src/environment/index.ts: -------------------------------------------------------------------------------- 1 | import { Environment } from "../types"; 2 | import { 3 | getBaseCommitHash, 4 | getBranchName, 5 | getCurrentCommitHash, 6 | getLatestBaseCommitHashes, 7 | } from "./git"; 8 | import { getRepositoryName } from "./repositoryName"; 9 | import { getRepositoryOwner } from "./repositoryOwner"; 10 | 11 | export * from "./repositoryName"; 12 | 13 | export async function getEnvironment(): Promise { 14 | const baseCommitHash = await getBaseCommitHash(); 15 | const currentCommitHash = await getCurrentCommitHash(); 16 | const latestBaseCommitHashes = await getLatestBaseCommitHashes(); 17 | const branchName = await getBranchName(); 18 | const isMainBranch = branchName === "main" || branchName === "master"; 19 | 20 | return { 21 | baseCommitHash, 22 | currentCommitHash, 23 | branchName, 24 | isMainBranch, 25 | latestBaseCommitHashes, 26 | repositoryName: await getRepositoryName(), 27 | repositoryOwner: await getRepositoryOwner(), 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/scooby-core/src/environment/repositoryName.ts: -------------------------------------------------------------------------------- 1 | import simpleGit from "simple-git"; 2 | 3 | export async function getRepositoryName(): Promise { 4 | const envRepositoryName = process.env["SCOOBY_REPOSITORY_NAME"]; 5 | if (envRepositoryName) { 6 | return envRepositoryName; 7 | } 8 | 9 | const gitRemoteName = await extractRepositoryNameFromGitRemote(); 10 | if (gitRemoteName) { 11 | return gitRemoteName; 12 | } 13 | 14 | throw new Error("unable to determine repository name"); 15 | } 16 | 17 | export async function extractRepositoryNameFromGitRemote(): Promise< 18 | string | undefined 19 | > { 20 | const git = simpleGit(); 21 | const remoteUrl = (await git.getConfig("remote.origin.url")).value; 22 | if (!remoteUrl) { 23 | return; 24 | } 25 | 26 | const nameRegex = /.*\/(.*)\.git$/; 27 | const match = nameRegex.exec(remoteUrl); 28 | if (!match?.[1]) { 29 | return; 30 | } 31 | 32 | return match[1]; 33 | } 34 | -------------------------------------------------------------------------------- /packages/scooby-core/src/environment/repositoryOwner.ts: -------------------------------------------------------------------------------- 1 | import simpleGit from "simple-git"; 2 | 3 | export async function getRepositoryOwner(): Promise { 4 | const envRepositoryOwner = process.env["SCOOBY_REPOSITORY_OWNER"]; 5 | if (envRepositoryOwner) { 6 | return envRepositoryOwner; 7 | } 8 | 9 | const gitRemoteOwner = await extractRepositoryOwnerFromGitRemote(); 10 | if (gitRemoteOwner) { 11 | return gitRemoteOwner; 12 | } 13 | 14 | throw new Error("unable to determine repository owner"); 15 | } 16 | 17 | export async function extractRepositoryOwnerFromGitRemote(): Promise< 18 | string | undefined 19 | > { 20 | const git = simpleGit(); 21 | const remoteUrl = (await git.getConfig("remote.origin.url")).value; 22 | if (!remoteUrl) { 23 | return; 24 | } 25 | 26 | const ownerRegex = /.*[\/:](.*)\/.*\.git$/; 27 | const match = ownerRegex.exec(remoteUrl); 28 | if (!match?.[1]) { 29 | return; 30 | } 31 | 32 | return match[1]; 33 | } 34 | -------------------------------------------------------------------------------- /packages/scooby-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { runReport } from "./reports"; 2 | export { updateStatus, runUpdateStatus } from "./status"; 3 | export { approveReports, runApproveReports } from "./review"; 4 | export * from "./types"; 5 | -------------------------------------------------------------------------------- /packages/scooby-core/src/loading/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | 3 | import { TestEntry } from "../types"; 4 | import { load } from "./loader/index"; 5 | 6 | export async function loadTestEntries( 7 | path: string, 8 | fileTypes: string[] 9 | ): Promise { 10 | if (!existsSync(path)) { 11 | throw new Error(`the given test path does not exist: '${path}'`); 12 | } 13 | 14 | const entries = await load(path, fileTypes); 15 | 16 | if (!entries.length) { 17 | throw new Error(`no test entries found in path: ${path}`); 18 | } 19 | 20 | return entries; 21 | } 22 | -------------------------------------------------------------------------------- /packages/scooby-core/src/loading/loader/util.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "path"; 2 | 3 | export function getExtension(path: string): string { 4 | const extension = parse(path).ext?.split(".")?.[1]; 5 | if (extension) { 6 | return extension; 7 | } 8 | 9 | throw new Error("unsupported entry type: " + path); 10 | } 11 | -------------------------------------------------------------------------------- /packages/scooby-core/src/matching/index.ts: -------------------------------------------------------------------------------- 1 | import { FidelityMatchingType } from "../types"; 2 | import { defaultMatchSources } from "./default"; 3 | import { flexibleMatchSources } from "./flexible"; 4 | import { MatchableSource, MatchedSources } from "./types"; 5 | import { validateSources } from "./validation"; 6 | export * from "./types"; 7 | 8 | export function matchSources( 9 | expected: T[], 10 | actual: T[], 11 | options?: { 12 | strategy?: FidelityMatchingType; 13 | } 14 | ): MatchedSources { 15 | validateSources(expected, actual); 16 | 17 | if (options?.strategy === "flexible") { 18 | return flexibleMatchSources(expected, actual); 19 | } else { 20 | return defaultMatchSources(expected, actual); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/scooby-core/src/matching/types.ts: -------------------------------------------------------------------------------- 1 | export type MatchableSource = { 2 | id: string; 3 | groupId: string; 4 | }; 5 | 6 | export type MatchedPair = { 7 | expected: T; 8 | actual: T; 9 | }; 10 | 11 | export type MatchedSources = { 12 | new: T[]; 13 | matching: MatchedPair[]; 14 | removed: T[]; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/scooby-core/src/matching/utils.ts: -------------------------------------------------------------------------------- 1 | import { MatchableSource } from "./types"; 2 | 3 | export function groupEntriesByGroupId( 4 | entries: T[] 5 | ): Map { 6 | const map: Map = new Map(); 7 | 8 | for (const entry of entries) { 9 | if (!map.has(entry.groupId)) { 10 | map.set(entry.groupId, []); 11 | } 12 | 13 | const list = map.get(entry.groupId); 14 | if (!list) { 15 | throw new Error("invariant violation, list has to be defined"); 16 | } 17 | 18 | list.push(entry); 19 | } 20 | 21 | return map; 22 | } 23 | -------------------------------------------------------------------------------- /packages/scooby-core/src/matching/validation.ts: -------------------------------------------------------------------------------- 1 | import { MatchableSource } from "./types"; 2 | 3 | export function validateSources( 4 | expected: T[], 5 | actual: T[] 6 | ) { 7 | checkNoDuplicateIds(expected, actual); 8 | } 9 | 10 | function checkNoDuplicateIds( 11 | expected: T[], 12 | actual: T[] 13 | ) { 14 | const expectedIds = new Set(expected.map((entry) => entry.id)); 15 | if (expected.length !== expectedIds.size) { 16 | throw new Error("expected source dataset has duplicate ids"); 17 | } 18 | 19 | const actualIds = new Set(actual.map((entry) => entry.id)); 20 | if (actual.length !== actualIds.size) { 21 | throw new Error("actual source dataset has duplicate ids"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/scooby-core/src/reports/fidelity-regression/report/results/util.ts: -------------------------------------------------------------------------------- 1 | import { BatchComparisonEntry } from "../../../../comparison"; 2 | 3 | export function getFidelityEntry( 4 | comparisons: Record, 5 | id: string 6 | ): T { 7 | if (!(id in comparisons)) { 8 | throw new Error( 9 | "unable to find matching fidelity comparison for id: " + id 10 | ); 11 | } 12 | 13 | return comparisons[id]; 14 | } 15 | -------------------------------------------------------------------------------- /packages/scooby-core/src/reports/fidelity/print.ts: -------------------------------------------------------------------------------- 1 | import { LocalFidelityReport } from "@animaapp/scooby-shared"; 2 | 3 | export function printFidelityReport(report: LocalFidelityReport) { 4 | console.log("################## FIDELITY TEST RESULTS ##################"); 5 | 6 | console.log("OVERALL FIDELITY: " + report.overallFidelityScore); 7 | 8 | const sortedPairs = [...report.pairs].sort( 9 | (a, b) => a.comparison.similarity - b.comparison.similarity 10 | ); 11 | 12 | console.log("Results (sorted by worse fidelity on top):"); 13 | console.table( 14 | sortedPairs.map((pair) => ({ 15 | id: pair.actual.id, 16 | fidelity: pair.comparison.similarity, 17 | outcome: pair.outcome, 18 | })) 19 | ); 20 | 21 | if (report.pairs.some((pair) => pair.outcome === "failure")) { 22 | console.log( 23 | "\nDETECTED FAILED ENTRIES! See the table above for more information" 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/scooby-core/src/reports/name.ts: -------------------------------------------------------------------------------- 1 | export function isValidName(string: string): boolean { 2 | return string.match(/^[a-z0-9-]+$/gi) !== null; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/src/reports/output/index.ts: -------------------------------------------------------------------------------- 1 | import { LocalReport, Report } from "@animaapp/scooby-shared"; 2 | import { ReportContext, ReportOutputTarget } from "../../types"; 3 | import { buildHostedReportOutput } from "./hosted"; 4 | import { buildZipReportOutput } from "./zip"; 5 | 6 | export async function buildReportOutput( 7 | report: LocalReport, 8 | context: ReportContext, 9 | outputTarget: ReportOutputTarget 10 | ): Promise { 11 | switch (outputTarget.type) { 12 | case "hosted": 13 | return buildHostedReportOutput(report, context); 14 | case "zip": 15 | return buildZipReportOutput(report, outputTarget); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/scooby-core/src/reports/shared/match-validation.ts: -------------------------------------------------------------------------------- 1 | import { MatchedSources } from "../../matching"; 2 | import { SourceEntry } from "../../types"; 3 | 4 | export function validateMatchedFidelitySources( 5 | matchedSources: MatchedSources 6 | ) { 7 | if (matchedSources.new.length > 0) { 8 | console.warn( 9 | "INVALID DATASET, detected actual entries not present in the expected dataset: ", 10 | matchedSources.new.map((entry) => entry.id) 11 | ); 12 | 13 | throw new Error( 14 | "invalid dataset, found actual test entries that are not present in the expected dataset" 15 | ); 16 | } 17 | 18 | if (matchedSources.removed.length > 0) { 19 | console.warn( 20 | "INVALID DATASET, missing actual test entries compared to the expected dataset: ", 21 | matchedSources.removed.map((entry) => entry.id) 22 | ); 23 | 24 | throw new Error( 25 | "invalid dataset, missing actual test entries compared to the expected dataset" 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/scooby-core/src/source/image/index.ts: -------------------------------------------------------------------------------- 1 | import { ImageSourceEntry, TestEntry } from "../../types"; 2 | import { generateHTMLImageSources } from "./html"; 3 | import { generatePNGImageSources } from "./png"; 4 | 5 | export type GenerateImageSourcesOptions = { 6 | maxThreads?: number; 7 | }; 8 | 9 | function getExtension(entries: TestEntry[]): string { 10 | const extensions = entries.map(({ extension }) => extension); 11 | 12 | if (extensions.every((val) => val === extensions[0])) { 13 | return extensions[0]; 14 | } 15 | 16 | throw new Error( 17 | "dataset is malformed, found different extensions of test entries." 18 | ); 19 | } 20 | 21 | export async function generateImageSources( 22 | entries: TestEntry[], 23 | options: GenerateImageSourcesOptions 24 | ): Promise { 25 | const extension = getExtension(entries); 26 | 27 | if (extension === "png") { 28 | return generatePNGImageSources(entries); 29 | } else if (extension === "html") { 30 | return generateHTMLImageSources(entries, options); 31 | } 32 | 33 | throw new Error( 34 | "cannot generate image sources, there is no handler for dataset type: " + 35 | extension 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /packages/scooby-core/src/source/image/png.ts: -------------------------------------------------------------------------------- 1 | import { ImageSourceEntry, TestEntry } from "../../types"; 2 | 3 | export async function generatePNGImageSources( 4 | entries: TestEntry[] 5 | ): Promise { 6 | const output: ImageSourceEntry[] = []; 7 | 8 | for (const entry of entries) { 9 | if (entry.extension !== "png") { 10 | throw new Error( 11 | "unable to load PNG test entry, found found entry type: " + 12 | entry.extension 13 | ); 14 | } 15 | 16 | output.push({ 17 | type: "image", 18 | id: entry.id, 19 | groupId: entry.id, 20 | path: entry.path, 21 | relativePath: entry.relativePath, 22 | tags: entry.options?.tags ?? [], 23 | metadata: entry.options?.metadata, 24 | }); 25 | } 26 | 27 | return output; 28 | } 29 | -------------------------------------------------------------------------------- /packages/scooby-core/src/status/reports.ts: -------------------------------------------------------------------------------- 1 | import { ScoobyAPI } from "@animaapp/scooby-api"; 2 | import { HostedReport } from "@animaapp/scooby-shared"; 3 | 4 | export async function fetchReports( 5 | api: ScoobyAPI, 6 | commit: string 7 | ): Promise { 8 | const reportsIds = await api.getReports({ 9 | commitHash: commit, 10 | }); 11 | 12 | const reports: HostedReport[] = []; 13 | 14 | for (const reportId of reportsIds) { 15 | const report = await api.getReport({ 16 | commitHash: commit, 17 | reportName: reportId, 18 | }); 19 | 20 | reports.push(report); 21 | } 22 | 23 | return reports; 24 | } 25 | -------------------------------------------------------------------------------- /packages/scooby-core/src/status/url.ts: -------------------------------------------------------------------------------- 1 | type GetReportURLContext = { 2 | repositoryName: string; 3 | commitHash: string; 4 | reportName: string; 5 | s3bucket: string; 6 | s3region: string; 7 | webBaseUrl: string; 8 | }; 9 | 10 | export function getURLForReport(context: GetReportURLContext): string { 11 | return `${context.webBaseUrl}/#/repo/${context.repositoryName}/commit/${context.commitHash}/report/${context.reportName}/?_s3_region=${context.s3region}&_s3_bucket=${context.s3bucket}`; 12 | } 13 | -------------------------------------------------------------------------------- /packages/scooby-core/src/utils/clone.ts: -------------------------------------------------------------------------------- 1 | export function clone(obj: T): T { 2 | return JSON.parse(JSON.stringify(obj)); 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/src/utils/commit.ts: -------------------------------------------------------------------------------- 1 | export function isRunningOnReferenceCommit( 2 | currentCommit: string, 3 | baseCommit: string 4 | ): boolean { 5 | return currentCommit === baseCommit; 6 | } 7 | -------------------------------------------------------------------------------- /packages/scooby-core/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | export function readEnvVariable(name: string): string { 2 | const value = process.env[name]; 3 | if (!value) { 4 | throw new Error( 5 | `unable to read '${name}' env variable, please make sure it's defined` 6 | ); 7 | } 8 | 9 | return value; 10 | } 11 | -------------------------------------------------------------------------------- /packages/scooby-core/src/utils/hash.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "node:crypto"; 2 | import { readFile } from "fs/promises"; 3 | 4 | export async function calculateFileMD5(path: string): Promise { 5 | const content = await readFile(path); 6 | return createHash("md5").update(content).digest("hex"); 7 | } 8 | -------------------------------------------------------------------------------- /packages/scooby-core/src/utils/resource.ts: -------------------------------------------------------------------------------- 1 | import { LocalResource } from "@animaapp/scooby-shared"; 2 | 3 | export function convertPathToLocalResource(path: string): LocalResource { 4 | return { 5 | type: "local", 6 | path, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/scooby-core/src/utils/temp.ts: -------------------------------------------------------------------------------- 1 | import { file, dir } from "tmp-promise"; 2 | 3 | export async function createTemporaryFile( 4 | prefix: string, 5 | postfix: string 6 | ): Promise { 7 | const { path } = await file({ prefix, postfix, keep: true }); 8 | return path; 9 | } 10 | 11 | export async function createTemporaryDirectory( 12 | prefix: string 13 | ): Promise { 14 | const { path } = await dir({ prefix, keep: true }); 15 | return path; 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-no-regression/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-no-regression/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-no-regression/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-no-regression/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1 (source of truth)

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-no-regression/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2 (source of truth)

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-no-regression/expected/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3 (source of truth)

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-no-regression/reference/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-no-regression/reference/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-no-regression/reference/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-with-regression/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2 (changed)

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-with-regression/actual/test4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 4 (new)

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-with-regression/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2 (source of truth)

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-with-regression/expected/test4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 4 (source of truth)

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-with-regression/reference/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/html-bad-fidelity-with-regression/reference/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-no-matching/actual/test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test3" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-no-matching/expected/test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "source-of-truth" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-no-matching/reference/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-no-matching/reference/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-with-regression/actual/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-with-regression/actual/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "changed", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-with-regression/actual/test4.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test4" 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-with-regression/expected/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true, 3 | "source-of-truth": true 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-with-regression/expected/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "changed", 3 | "name": "test2", 4 | "source-of-truth": true 5 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-with-regression/expected/test4.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test4", 3 | "source-of-truth": true 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-with-regression/reference/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-with-regression/reference/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity-regression/json-bad-fidelity-with-regression/reference/test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test3" 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-perfect-fidelity/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-perfect-fidelity/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-perfect-fidelity/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-perfect-fidelity/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-perfect-fidelity/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-perfect-fidelity/expected/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-with-differences/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-with-differences/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 |

This is a difference!

10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-with-differences/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-with-differences/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-with-differences/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/html-with-differences/expected/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/json-perfect-fidelity/actual/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/json-perfect-fidelity/actual/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/json-perfect-fidelity/expected/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/json-perfect-fidelity/expected/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/json-with-differences/actual/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": false 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/json-with-differences/actual/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "changed", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/json-with-differences/expected/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/json-with-differences/expected/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/jsx-perfect-fidelity/actual/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/jsx-perfect-fidelity/actual/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/jsx-perfect-fidelity/expected/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/jsx-perfect-fidelity/expected/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/jsx-with-differences/actual/test1.jsx: -------------------------------------------------------------------------------- 1 | function Goodbye(props) { 2 | return

Goodbye, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/jsx-with-differences/actual/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/jsx-with-differences/expected/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/jsx-with-differences/expected/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/non-matching/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/non-matching/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/non-matching/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/non-matching/expected/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/unformatted-equal-json/actual/test1.json: -------------------------------------------------------------------------------- 1 | {"test": true 2 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/unformatted-equal-json/actual/test2.json: -------------------------------------------------------------------------------- 1 | {"another": "test", "name": "test2"} -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/unformatted-equal-json/expected/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/fidelity/unformatted-equal-json/expected/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-metadata/test1-image.scooby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/basic-metadata/test1-image.scooby.png -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-metadata/test1.code.scooby.jsx: -------------------------------------------------------------------------------- 1 | function Goodbye(props) { 2 | return

Goodbye, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-metadata/test1.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/basic-metadata/test1.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-metadata/test1.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "type": "text", 5 | "name": "Example text", 6 | "text": "text metadata" 7 | }, 8 | { 9 | "type": "link", 10 | "name": "Example link", 11 | "description": "it even has a description", 12 | "url": "https://google.com" 13 | }, 14 | { 15 | "type": "image", 16 | "name": "Example image", 17 | "image_path": "test1-image.scooby.png" 18 | }, 19 | { 20 | "type": "file", 21 | "name": "Example file", 22 | "file_path": "./test1-image.scooby.png" 23 | }, 24 | { 25 | "type": "code", 26 | "name": "Example code", 27 | "code_path": "./test1.code.scooby.jsx" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-metadata/test2-code.scooby.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-metadata/test2.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/basic-metadata/test2.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-metadata/test2.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "type": "code", 5 | "name": "Example code", 6 | "code_path": "./test2-code.scooby.html" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-test-structure/test1.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/basic-test-structure/test1.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-test-structure/test1.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewports": [ 3 | { 4 | "width": 1920, 5 | "height": 1080 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/basic-test-structure/test2.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/basic-test-structure/test2.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/invalid-metadata-path/test1.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/invalid-metadata-path/test1.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/invalid-metadata-path/test1.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "type": "image", 5 | "name": "Example image", 6 | "image_path": "invalid-path.png" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/multiple-file-types/test1.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/multiple-file-types/test1.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/multiple-file-types/test1.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/multiple-file-types/test2.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/multiple-file-types/test2.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-metadata/assets.scooby/code.js: -------------------------------------------------------------------------------- 1 | console.log('test'); -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-metadata/test/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/nested-metadata/test/index.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-metadata/test/scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "type": "code", 5 | "name": "example code", 6 | "code_path": "../assets.scooby/code.js" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test1/another.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test1/another.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test1/another.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewports": [ 3 | { 4 | "width": 400, 5 | "height": 400 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test1/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test1/index.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test1/index.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewports": [ 3 | { 4 | "width": 300, 5 | "height": 300 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test2/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test2/index.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test2/scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewports": [ 3 | { 4 | "width": 1920, 5 | "height": 1080 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test3/file.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/nested-multiple-files-test-structure/test3/file.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-test-structure/test1/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/nested-test-structure/test1/index.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-test-structure/test2/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/nested-test-structure/test2/index.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/nested-test-structure/test2/scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewports": [ 3 | { 4 | "width": 1920, 5 | "height": 1080 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test1/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test1/index.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test1/to-skip.scooby.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test1/to-skip.scooby.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test1/toskip.scooby/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test1/toskip.scooby/index.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test2/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test2/index.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test2/index.scooby.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test2/index.scooby.html -------------------------------------------------------------------------------- /packages/scooby-core/test/data/loading/skip-scooby-entries-correctly/test2/scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewports": [ 3 | { 4 | "width": 1920, 5 | "height": 1080 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-no-matching/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-no-matching/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-no-matching/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-no-regressions/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-no-regressions/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-no-regressions/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-no-regressions/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-regression-with-differences/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-regression-with-differences/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 |

This has changed

10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-regression-with-differences/actual/test4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

This is a new test

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-regression-with-differences/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-regression-with-differences/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/html-regression-with-differences/expected/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-matching/actual/test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test3" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-matching/expected/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-matching/expected/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-regression-if-formatted/actual/test1.json: -------------------------------------------------------------------------------- 1 | {"test": true 2 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-regression-if-formatted/actual/test2.json: -------------------------------------------------------------------------------- 1 | {"another": "test", "name": "test2"} -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-regression-if-formatted/expected/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-regression-if-formatted/expected/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-regression/actual/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-regression/actual/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-regression/expected/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-no-regression/expected/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-with-regression/actual/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-with-regression/actual/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "changed", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-with-regression/actual/test4.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test3" 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-with-regression/expected/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-with-regression/expected/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/json-with-regression/expected/test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test3" 3 | } -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/jsx-no-regression/actual/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/jsx-no-regression/actual/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/jsx-no-regression/expected/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/jsx-no-regression/expected/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/jsx-with-regression/actual/test1.jsx: -------------------------------------------------------------------------------- 1 | function Goodbye(props) { 2 | return

Goodbye, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/jsx-with-regression/actual/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/jsx-with-regression/expected/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-core/test/data/regression/jsx-with-regression/expected/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-core/test/name.test.ts: -------------------------------------------------------------------------------- 1 | import { isValidName } from "../src/reports/name"; 2 | 3 | describe("naming", () => { 4 | it("validates names correctly", () => { 5 | expect(isValidName("valid")).toBeTruthy(); 6 | expect(isValidName("valid-name")).toBeTruthy(); 7 | expect(isValidName("valid-123")).toBeTruthy(); 8 | expect(isValidName("VALID")).toBeTruthy(); 9 | expect(isValidName("Valid")).toBeTruthy(); 10 | 11 | expect(isValidName("invalid name")).toBeFalsy(); 12 | expect(isValidName("invalid/")).toBeFalsy(); 13 | expect(isValidName("@invalid")).toBeFalsy(); 14 | expect(isValidName("invalid.name")).toBeFalsy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/scooby-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react", 5 | "moduleResolution": "node", 6 | "module": "CommonJS", 7 | "resolveJsonModule": true, 8 | "outDir": "./dist", 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "composite": true, 12 | "strict": true, 13 | "lib": ["es2019"], 14 | "allowSyntheticDefaultImports": true, 15 | "skipLibCheck": true, 16 | "paths": { 17 | "@animaapp/scooby-api": ["../scooby-api"], 18 | "@animaapp/scooby-github-api": ["../scooby-github-api"], 19 | "@animaapp/scooby-shared": ["../scooby-shared"] 20 | } 21 | }, 22 | "exclude": ["node_modules", "__tests__", "dist", "test"], 23 | "references": [ 24 | { "path": "../scooby-api" }, 25 | { "path": "../scooby-github-api" }, 26 | { "path": "../scooby-shared" } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/scooby-github-api/README.md: -------------------------------------------------------------------------------- 1 | # Scooby 2 | 3 | This package is part of the [Scooby](https://github.com/AnimaApp/scooby) project, 4 | an optimized regression and fidelity testing framework. 5 | 6 | For more information, please visit the [documentation](https://github.com/AnimaApp/scooby). 7 | -------------------------------------------------------------------------------- /packages/scooby-github-api/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testPathIgnorePatterns: ["/node_modules/", ".*shared.test.*"], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/scooby-github-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animaapp/scooby-github-api", 3 | "version": "1.27.0", 4 | "description": "This package contains the logic to interact with GitHub API", 5 | "main": "dist/src/index.js", 6 | "repository": "https://github.com/AnimaApp/scooby", 7 | "license": "MIT", 8 | "publishConfig": { 9 | "registry": "https://npm.pkg.github.com" 10 | }, 11 | "scripts": { 12 | "build": "tsc --build", 13 | "clean": "rm -Rf ./dist", 14 | "test": "jest --passWithNoTests" 15 | }, 16 | "devDependencies": { 17 | "@types/jest": "^27.4.1", 18 | "@types/node": "^18.8.2", 19 | "jest": "^29.2.1", 20 | "ts-jest": "^29.0.3" 21 | }, 22 | "dependencies": { 23 | "@octokit/core": "^4.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/scooby-github-api/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { GitHubAPIOptions } from "../options"; 2 | import { GitHubAPI } from "../types"; 3 | import { OctokitGitHubAPI } from "./octokit"; 4 | 5 | export function getRestGitHubAPI(options: GitHubAPIOptions): GitHubAPI { 6 | return new OctokitGitHubAPI(options); 7 | } 8 | -------------------------------------------------------------------------------- /packages/scooby-github-api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { GitHubAPIOptions, prepareOptions } from "./options"; 2 | import { GitHubAPI } from "./types"; 3 | import { getRestGitHubAPI } from "./api"; 4 | 5 | export async function getGitHubAPI( 6 | options?: Partial 7 | ): Promise { 8 | const effectiveOptions = prepareOptions(options); 9 | 10 | return getRestGitHubAPI(effectiveOptions); 11 | } 12 | -------------------------------------------------------------------------------- /packages/scooby-github-api/src/options.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const optionsSchema = z.object({ 4 | owner: z.string(), 5 | repository: z.string(), 6 | accessToken: z.string(), 7 | }); 8 | 9 | export type GitHubAPIOptions = z.infer; 10 | 11 | export function prepareOptions( 12 | options: Partial | undefined 13 | ): GitHubAPIOptions { 14 | const validatedOptions = optionsSchema.parse({ 15 | ...getOptionsFromEnvVariables(), 16 | ...options, 17 | }); 18 | 19 | return validatedOptions; 20 | } 21 | 22 | function getOptionsFromEnvVariables(): Partial { 23 | return { 24 | repository: process.env?.["SCOOBY_REPOSITORY_NAME"], 25 | owner: process.env?.["SCOOBY_REPOSITORY_OWNER"], 26 | accessToken: process.env?.["SCOOBY_GITHUB_ACCESS_TOKEN"], 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/scooby-github-api/src/types.ts: -------------------------------------------------------------------------------- 1 | export type GitHubAPI = { 2 | getAssociatedPR(commit: string): Promise; 3 | getAssociatedCommits(commit: string): Promise; 4 | postCommitStatus(commit: string, status: CommitStatus): Promise; 5 | hasPRBeenApproved(pr: number): Promise; 6 | }; 7 | 8 | export type CommitStatus = { 9 | name: string; 10 | state: "success" | "failure" | "pending"; 11 | targetUrl: string; 12 | description: string; 13 | }; 14 | 15 | export type PRApprovalStatus = 16 | | { approved: true; user?: string } 17 | | { approved: false }; 18 | -------------------------------------------------------------------------------- /packages/scooby-github-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react", 5 | "moduleResolution": "node", 6 | "module": "CommonJS", 7 | "resolveJsonModule": true, 8 | "outDir": "./dist", 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "composite": true, 12 | "strict": true, 13 | "lib": ["es2019"], 14 | "allowSyntheticDefaultImports": true, 15 | "skipLibCheck": true 16 | }, 17 | "exclude": ["node_modules", "__tests__", "dist", "test"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/scooby-service/README.md: -------------------------------------------------------------------------------- 1 | # Scooby 2 | 3 | This package is part of the [Scooby](https://github.com/AnimaApp/scooby) project, 4 | an optimized regression and fidelity testing framework. 5 | 6 | For more information, please visit the [documentation](https://github.com/AnimaApp/scooby). 7 | -------------------------------------------------------------------------------- /packages/scooby-service/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testPathIgnorePatterns: ["/node_modules/", ".*shared.test.*"], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/scooby-service/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import bearerAuthPlugin from "@fastify/bearer-auth"; 2 | import { FastifyPluginCallback } from "fastify"; 3 | import { getAuthToken } from "../auth"; 4 | import { contextProvider } from "../context"; 5 | import reviewRoute from "./review"; 6 | 7 | const AUTH_KEYS = new Set([getAuthToken()]); 8 | 9 | export const apiRoute: FastifyPluginCallback = (fastify, _, done) => { 10 | fastify.register(bearerAuthPlugin, { keys: AUTH_KEYS }); 11 | fastify.register(contextProvider); 12 | 13 | fastify.register(reviewRoute, { prefix: "/review" }); 14 | 15 | done(); 16 | }; 17 | 18 | export default apiRoute; 19 | -------------------------------------------------------------------------------- /packages/scooby-service/src/auth.ts: -------------------------------------------------------------------------------- 1 | export function getAuthToken(): string { 2 | const token = process.env["SCOOBY_SERVICE_ACCESS_TOKEN"]; 3 | if (!token) { 4 | throw new Error( 5 | "could not get access token, please provide the SCOOBY_SERVICE_ACCESS_TOKEN env variable" 6 | ); 7 | } 8 | 9 | return token; 10 | } 11 | -------------------------------------------------------------------------------- /packages/scooby-service/src/env.ts: -------------------------------------------------------------------------------- 1 | export function readEnvVariable(name: string): string { 2 | const value = process.env[name]; 3 | if (!value) { 4 | throw new Error( 5 | `unable to read '${name}' env variable, please make sure it's defined` 6 | ); 7 | } 8 | 9 | return value; 10 | } 11 | -------------------------------------------------------------------------------- /packages/scooby-service/src/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | require("dotenv").config({ path: require("find-config")(".env") }); 3 | 4 | import fastify from "fastify"; 5 | import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox"; 6 | import cors from "@fastify/cors"; 7 | import apiRoute from "./api"; 8 | 9 | const server = fastify({ 10 | logger: true, 11 | }).withTypeProvider(); 12 | server.register(cors, { 13 | allowedHeaders: ["Content-Type", "Authorization"], 14 | origin: "*", 15 | }); 16 | 17 | server.get("/", async () => { 18 | return { up: true }; 19 | }); 20 | 21 | server.get("/up", async () => { 22 | return { up: true }; 23 | }); 24 | 25 | server.get("/gitver", (_, res) => { 26 | res.redirect( 27 | `https://www.github.com/AnimaApp/${process.env.REPO_NAME}/commit/${process.env.GIT_VERSION}` 28 | ); 29 | }); 30 | 31 | server.register(apiRoute, { prefix: "/api" }); 32 | 33 | server.listen({ port: 3000, host: "0.0.0.0" }, (err, address) => { 34 | if (err) { 35 | console.error(err); 36 | process.exit(1); 37 | } 38 | console.log(`Scooby Service listening at ${address}`); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/scooby-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react", 5 | "moduleResolution": "node", 6 | "module": "CommonJS", 7 | "resolveJsonModule": true, 8 | "outDir": "./dist", 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "composite": true, 12 | "strict": true, 13 | "lib": ["es2019"], 14 | "allowSyntheticDefaultImports": true, 15 | "skipLibCheck": true, 16 | "paths": { 17 | "@animaapp/scooby-api": ["../scooby-api"], 18 | "@animaapp/scooby-core": ["../scooby-core"], 19 | "@animaapp/scooby-github-api": ["../scooby-github-api"], 20 | "@animaapp/scooby-shared": ["../scooby-shared"] 21 | } 22 | }, 23 | "exclude": ["node_modules", "__tests__", "dist", "test"], 24 | "references": [ 25 | { "path": "../scooby-api" }, 26 | { "path": "../scooby-core" }, 27 | { "path": "../scooby-github-api" }, 28 | { "path": "../scooby-shared" } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/scooby-shared/README.md: -------------------------------------------------------------------------------- 1 | # Scooby 2 | 3 | This package is part of the [Scooby](https://github.com/AnimaApp/scooby) project, 4 | an optimized regression and fidelity testing framework. 5 | 6 | For more information, please visit the [documentation](https://github.com/AnimaApp/scooby). 7 | -------------------------------------------------------------------------------- /packages/scooby-shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animaapp/scooby-shared", 3 | "version": "1.27.0", 4 | "description": "Code that should be shared between backend and frontend", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "https://github.com/AnimaApp/scooby", 8 | "license": "MIT", 9 | "publishConfig": { 10 | "registry": "https://npm.pkg.github.com" 11 | }, 12 | "scripts": { 13 | "build": "tsc", 14 | "clean": "rm -Rf ./dist && rm -f tsconfig.tsbuildinfo" 15 | }, 16 | "dependencies": { 17 | "zod": "^3.19.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/scooby-shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./parsing"; 2 | export * from "./types"; 3 | export * from "./s3"; 4 | -------------------------------------------------------------------------------- /packages/scooby-shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react", 5 | "moduleResolution": "node", 6 | "module": "CommonJS", 7 | "declaration": true, 8 | "resolveJsonModule": true, 9 | "composite": true, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "strict": true, 13 | "lib": ["es2019"], 14 | "allowSyntheticDefaultImports": true, 15 | "skipLibCheck": true 16 | }, 17 | "exclude": ["node_modules", "__tests__", "dist"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/scooby-web/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "*.svg": [ 5 | "...", 6 | "@parcel/transformer-svg-react" 7 | ] 8 | } 9 | } -------------------------------------------------------------------------------- /packages/scooby-web/README.md: -------------------------------------------------------------------------------- 1 | # Scooby 2 | 3 | This package is part of the [Scooby](https://github.com/AnimaApp/scooby) project, 4 | an optimized regression and fidelity testing framework. 5 | 6 | For more information, please visit the [documentation](https://github.com/AnimaApp/scooby). 7 | -------------------------------------------------------------------------------- /packages/scooby-web/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testPathIgnorePatterns: ["/node_modules/", ".*shared.test.*"], 6 | testTimeout: 120000, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/scooby-web/scripts/prepare-env-variables.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | require("dotenv").config({ path: require("find-config")(".env") }); 3 | 4 | const path = require("path"); 5 | const fs = require("fs/promises"); 6 | 7 | const ENV_VARIABLES = [ 8 | "SCOOBY_SERVICE_ACCESS_TOKEN", 9 | "SCOOBY_SERVICE_BASE_URL", 10 | ]; 11 | 12 | async function main() { 13 | const variables = {}; 14 | 15 | for (const variable of ENV_VARIABLES) { 16 | const value = process.env[variable]; 17 | if (!value) { 18 | throw new Error( 19 | `could not resolve env variable '${variable}', please make sure to specify it` 20 | ); 21 | } 22 | 23 | variables[variable] = value; 24 | } 25 | 26 | const content = Object.entries(variables) 27 | .map(([key, value]) => `${key}=${value}`) 28 | .join("\n"); 29 | const targetEnvFile = path.join(__dirname, "../.env"); 30 | await fs.writeFile(targetEnvFile, content, "utf-8"); 31 | } 32 | 33 | main(); 34 | -------------------------------------------------------------------------------- /packages/scooby-web/src/assets/scoobyLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-web/src/assets/scoobyLogo.png -------------------------------------------------------------------------------- /packages/scooby-web/src/components/CodeComparator/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | CodeComparatorController as CodeComparator, 3 | CodeData, 4 | } from "./CodeComparatorController"; 5 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/CodeComparator/useSource.ts: -------------------------------------------------------------------------------- 1 | import { useURL } from "../../data-fetching/hooks/useURL"; 2 | 3 | export type Sources = { 4 | actual?: string; 5 | expected?: string; 6 | diff?: string; 7 | }; 8 | 9 | export function useSources(sources: { 10 | expectedUrl?: string; 11 | actualUrl?: string; 12 | diffUrl?: string; 13 | }): { 14 | sources?: Sources; 15 | isLoading?: boolean; 16 | error?: unknown; 17 | } { 18 | const { 19 | source: expectedSource, 20 | isLoading: expectedLoading, 21 | error: expectedError, 22 | } = useURL(sources.expectedUrl ?? null); 23 | const { 24 | source: actualSource, 25 | isLoading: actualLoading, 26 | error: actualError, 27 | } = useURL(sources.actualUrl ?? null); 28 | const { 29 | source: diffSource, 30 | isLoading: diffLoading, 31 | error: diffError, 32 | } = useURL(sources.diffUrl ?? null); 33 | 34 | return { 35 | sources: { 36 | expected: expectedSource, 37 | actual: actualSource, 38 | diff: diffSource, 39 | }, 40 | isLoading: expectedLoading || actualLoading || diffLoading, 41 | error: expectedError || actualError || diffError, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/EnhancedLink.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Link, RelativeRoutingType, To } from "react-router-dom"; 3 | import { getProtectedParams } from "../routes/hooks/params"; 4 | import { useQueryParams } from "../routes/hooks/useQueryParams"; 5 | 6 | type Props = { 7 | to: To; 8 | relative?: RelativeRoutingType; 9 | children?: ReactNode; 10 | }; 11 | 12 | // A version of Link that preserves global parameters 13 | export const EnhancedLink = (props: Props) => { 14 | const params = useQueryParams>(); 15 | const protectedParams = getProtectedParams(params); 16 | const serializedParams = new URLSearchParams(protectedParams).toString(); 17 | const toEnhanced = `${props.to}?${serializedParams}`; 18 | 19 | return ( 20 | 21 | {props.children} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/EntryList/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./EntryList"; 2 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/EntryList/modes/badge.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CheckCircleFilled, 3 | ExclamationCircleOutlined, 4 | } from "@ant-design/icons"; 5 | import { Tooltip } from "antd"; 6 | import { EntryStatus } from "../../../types"; 7 | 8 | export function getStatusBadge( 9 | status: EntryStatus | undefined 10 | ): JSX.Element | null { 11 | if (status === "approved") { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } else if (status === "changes_requested") { 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | return null; 26 | } 27 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/EntryList/modes/basic/index.tsx: -------------------------------------------------------------------------------- 1 | import { List } from "antd"; 2 | import { Entry } from "../../../../types"; 3 | import { ListItem } from "./ListItem"; 4 | 5 | type Props = { 6 | selectedEntryId?: string; 7 | entries: Entry[]; 8 | onEntrySelected: (entry: Entry) => void; 9 | }; 10 | 11 | export const BasicEntryList = ({ 12 | entries, 13 | selectedEntryId, 14 | onEntrySelected, 15 | }: Props) => { 16 | return ( 17 |
18 | ( 24 | onEntrySelected(entry)} 29 | /> 30 | )} 31 | /> 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/EntryList/modes/image/index.tsx: -------------------------------------------------------------------------------- 1 | import { List } from "antd"; 2 | import { ImageEntry } from "../../../../types"; 3 | import { ListItem } from "./ListItem"; 4 | 5 | type Props = { 6 | selectedEntryId?: string; 7 | entries: ImageEntry[]; 8 | onEntrySelected: (entry: ImageEntry) => void; 9 | }; 10 | 11 | export const ImageEntryList = ({ 12 | entries, 13 | selectedEntryId, 14 | onEntrySelected, 15 | }: Props) => { 16 | return ( 17 |
18 | ( 23 | onEntrySelected(entry)} 28 | /> 29 | )} 30 | /> 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/EntryList/modes/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApartmentOutlined, 3 | PictureOutlined, 4 | UnorderedListOutlined, 5 | } from "@ant-design/icons"; 6 | import { BasicEntryList } from "./basic"; 7 | import { FileTreeEntryList } from "./file-tree"; 8 | import { ImageEntryList } from "./image"; 9 | 10 | export const ENTRY_LIST_MODES = { 11 | image: { 12 | name: "Image View", 13 | icon: PictureOutlined, 14 | description: "View the test entries as a list of images", 15 | render: ImageEntryList, 16 | } as const, 17 | basic: { 18 | name: "Basic View", 19 | icon: UnorderedListOutlined, 20 | description: "View the test entries as a basic list of items", 21 | render: BasicEntryList, 22 | } as const, 23 | fileTree: { 24 | name: "File Tree View", 25 | icon: ApartmentOutlined, 26 | description: "View the test entries as a file tree", 27 | render: FileTreeEntryList, 28 | } as const, 29 | } as const; 30 | 31 | export type Mode = keyof typeof ENTRY_LIST_MODES; 32 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/ErrorPanel.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | title?: string; 3 | message: string; 4 | }; 5 | 6 | export default function ErrorPanel(props: Props) { 7 | return ( 8 |
9 |

{props.title ?? "Oh snap! Something went wrong"}

10 |

{props.message}

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/ImageComparator/ImageComparatorController.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import useHotkeys from "@reecelucas/react-use-hotkeys"; 3 | import { ImageComparator, PreferredMode } from "./ImageComparator"; 4 | import { Sentiment } from "@animaapp/scooby-shared"; 5 | 6 | type Props = { 7 | name: string; 8 | data: ImageData; 9 | }; 10 | 11 | type BaseImageData = { 12 | tag?: string; 13 | sentiment?: Sentiment; 14 | }; 15 | export type ImageData = BaseImageData & 16 | ( 17 | | { 18 | type: "pair"; 19 | expectedUrl: string; 20 | actualUrl: string; 21 | diffUrl: string; 22 | overlapUrl: string; 23 | similarity: number; 24 | } 25 | | { 26 | type: "new"; 27 | newUrl: string; 28 | } 29 | | { 30 | type: "removed"; 31 | removedUrl: string; 32 | } 33 | ); 34 | 35 | export const ImageComparatorController = (props: Props) => { 36 | const [mode, setMode] = useState("diff"); 37 | 38 | useHotkeys("ArrowLeft", () => setMode("expected")); 39 | useHotkeys("ArrowRight", () => setMode("actual")); 40 | useHotkeys("d", () => setMode("diff")); 41 | useHotkeys("o", () => setMode("overlap")); 42 | 43 | return ( 44 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/ImageComparator/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ImageComparatorController as ImageComparator, 3 | ImageData, 4 | } from "./ImageComparatorController"; 5 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import "./loader.css"; 2 | 3 | export default function Loader() { 4 | return ( 5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/MetadataList/ListItem/BaseListItem.tsx: -------------------------------------------------------------------------------- 1 | import { InfoCircleOutlined } from "@ant-design/icons"; 2 | import { Card, Tooltip } from "antd"; 3 | import { PropsWithChildren } from "react"; 4 | 5 | type Props = PropsWithChildren<{ 6 | name: string; 7 | description?: string; 8 | }>; 9 | 10 | export const BaseListItem = ({ name, description, children }: Props) => { 11 | return ( 12 | 17 | 18 | 19 | ) : undefined 20 | } 21 | size="small" 22 | bodyStyle={{ padding: 8, position: "relative" }} 23 | style={{ 24 | marginBottom: 8, 25 | }} 26 | > 27 | {children} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/MetadataList/ListItem/FileListItem.tsx: -------------------------------------------------------------------------------- 1 | import { FileMetadata, HostedResource } from "@animaapp/scooby-shared"; 2 | import { DownloadOutlined } from "@ant-design/icons"; 3 | import { Button } from "antd"; 4 | import { PropsWithChildren } from "react"; 5 | import { BaseListItem } from "./BaseListItem"; 6 | 7 | type Props = PropsWithChildren<{ 8 | metadata: FileMetadata; 9 | }>; 10 | 11 | export const FileListItem = ({ metadata }: Props) => { 12 | return ( 13 | 14 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/MetadataList/ListItem/ImageListItem.tsx: -------------------------------------------------------------------------------- 1 | import { HostedResource, ImageMetadata } from "@animaapp/scooby-shared"; 2 | import { Image } from "antd"; 3 | import { PropsWithChildren } from "react"; 4 | import { BaseListItem } from "./BaseListItem"; 5 | 6 | type Props = PropsWithChildren<{ 7 | metadata: ImageMetadata; 8 | }>; 9 | 10 | export const ImageListItem = ({ metadata }: Props) => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/MetadataList/ListItem/LinkListItem.tsx: -------------------------------------------------------------------------------- 1 | import { LinkMetadata } from "@animaapp/scooby-shared"; 2 | import { LinkOutlined } from "@ant-design/icons"; 3 | import { Button } from "antd"; 4 | import { PropsWithChildren } from "react"; 5 | import { BaseListItem } from "./BaseListItem"; 6 | 7 | type Props = PropsWithChildren<{ 8 | metadata: LinkMetadata; 9 | }>; 10 | 11 | export const LinkListItem = ({ metadata }: Props) => { 12 | return ( 13 | 14 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/MetadataList/ListItem/TextListItem.tsx: -------------------------------------------------------------------------------- 1 | import { TextMetadata } from "@animaapp/scooby-shared"; 2 | import { PropsWithChildren } from "react"; 3 | import { BaseListItem } from "./BaseListItem"; 4 | 5 | type Props = PropsWithChildren<{ 6 | metadata: TextMetadata; 7 | }>; 8 | 9 | export const TextListItem = ({ metadata }: Props) => { 10 | return ( 11 | 12 | {metadata.text} 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/MetadataList/ListItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { HostedResource, Metadata } from "@animaapp/scooby-shared"; 2 | import { CodeListItem } from "./CodeListItem"; 3 | import { FileListItem } from "./FileListItem"; 4 | import { ImageListItem } from "./ImageListItem"; 5 | import { LinkListItem } from "./LinkListItem"; 6 | import { TextListItem } from "./TextListItem"; 7 | 8 | type Props = { 9 | metadata: Metadata; 10 | }; 11 | 12 | export const ListItem = ({ metadata }: Props) => { 13 | switch (metadata.type) { 14 | case "text": 15 | return ; 16 | case "link": 17 | return ; 18 | case "image": 19 | return ; 20 | case "file": 21 | return ; 22 | case "code": 23 | return ; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/MetadataList/MetadataList.tsx: -------------------------------------------------------------------------------- 1 | import { HostedResource, Metadata } from "@animaapp/scooby-shared"; 2 | import { List } from "antd"; 3 | import { ListItem } from "./ListItem"; 4 | 5 | type Props = { 6 | metadata: Metadata[]; 7 | }; 8 | 9 | export const MetadataList = ({ metadata }: Props) => { 10 | return ( 11 |
12 | ( 17 | 18 | )} 19 | locale={{ 20 | emptyText: 21 | "There is no metadata defined for this entry, check out the documentation to see how to add it.", 22 | }} 23 | /> 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/MetadataList/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MetadataList"; 2 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/StatsView/FractionStatisticView.tsx: -------------------------------------------------------------------------------- 1 | import { FractionStatistic } from "@animaapp/scooby-shared"; 2 | import { StatisticView } from "./StatisticView"; 3 | 4 | type Props = { 5 | statistic: FractionStatistic; 6 | compact?: boolean; 7 | }; 8 | 9 | export const FractionStatisticView = ({ statistic, compact }: Props) => { 10 | return ( 11 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/StatsView/GaugeStatisticView.tsx: -------------------------------------------------------------------------------- 1 | import { GaugeStatistic } from "@animaapp/scooby-shared"; 2 | import { StatisticView } from "./StatisticView"; 3 | 4 | type Props = { 5 | statistic: GaugeStatistic; 6 | compact?: boolean; 7 | }; 8 | 9 | export const GaugeStatisticView = ({ statistic, compact }: Props) => { 10 | return ( 11 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/StatsView/StatisticView.tsx: -------------------------------------------------------------------------------- 1 | import { SummaryStatistic } from "@animaapp/scooby-shared"; 2 | import { Statistic, Tag, Tooltip } from "antd"; 3 | import { ReactNode } from "react"; 4 | import { capitalize } from "../../utils/capitalize"; 5 | import { getColorForSentiment } from "../../utils/colors"; 6 | 7 | type Props = { 8 | statistic: SummaryStatistic; 9 | compact?: boolean; 10 | value?: string | number; 11 | suffix?: ReactNode; 12 | prefix?: ReactNode; 13 | }; 14 | 15 | export const StatisticView = ({ 16 | statistic, 17 | value, 18 | suffix, 19 | prefix, 20 | compact, 21 | }: Props) => { 22 | const color = getColorForSentiment(statistic.sentiment); 23 | 24 | return ( 25 | 26 | {compact ? ( 27 | 28 | {capitalize(statistic.name)}: {value} 29 | {suffix} 30 | 31 | ) : ( 32 | 41 | )} 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/StatsView/StatsView.tsx: -------------------------------------------------------------------------------- 1 | import { SummaryStatistic } from "@animaapp/scooby-shared"; 2 | import { FractionStatisticView } from "./FractionStatisticView"; 3 | import { GaugeStatisticView } from "./GaugeStatisticView"; 4 | 5 | type Props = { 6 | stats: SummaryStatistic[]; 7 | compact?: boolean; 8 | }; 9 | 10 | export const StatsView = (props: Props) => { 11 | if (props.stats.length === 0) { 12 | return No statistics have been generated for this report.; 13 | } 14 | 15 | return ( 16 |
17 | {props.stats.map((stat) => { 18 | if (stat.type === "fraction") { 19 | return ( 20 | 25 | ); 26 | } else if (stat.type === "gauge") { 27 | return ( 28 | 33 | ); 34 | } 35 | })} 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/scooby-web/src/components/StatsView/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./StatsView"; 2 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/api/index.ts: -------------------------------------------------------------------------------- 1 | import { GlobalEnvironmentSetup } from "@animaapp/scooby-shared"; 2 | import { MixedScoobyWebAPI } from "./mixed"; 3 | import { ScoobyWebAPI } from "./types"; 4 | import { ZipScoobyWebAPI } from "./zip"; 5 | 6 | export * from "./types"; 7 | 8 | export type APICreationOptions = { 9 | environment: GlobalEnvironmentSetup; 10 | }; 11 | 12 | export function createAPI( 13 | options: APICreationOptions 14 | ): ScoobyWebAPI | undefined { 15 | if (options.environment.s3) { 16 | return new MixedScoobyWebAPI(options); 17 | } 18 | if (options.environment.zipArchive) { 19 | return new ZipScoobyWebAPI(options.environment.zipArchive.buffer); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/api/mixed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HostedReport, 3 | CommitStatusOverview, 4 | Review, 5 | } from "@animaapp/scooby-shared"; 6 | import { APICreationOptions } from "."; 7 | import { ReportContext } from "./types"; 8 | import { RestScoobyWebAPI } from "./rest"; 9 | import { S3ScoobyWebAPI } from "./s3"; 10 | import { CommitContext, ScoobyWebAPI } from "./types"; 11 | 12 | export class MixedScoobyWebAPI implements ScoobyWebAPI { 13 | private s3Api: S3ScoobyWebAPI; 14 | private restApi: RestScoobyWebAPI; 15 | 16 | constructor(options: APICreationOptions) { 17 | this.s3Api = new S3ScoobyWebAPI(options); 18 | this.restApi = new RestScoobyWebAPI(options); 19 | } 20 | 21 | getReports(params: CommitContext): Promise { 22 | return this.s3Api.getReports(params); 23 | } 24 | getReport(params: ReportContext): Promise { 25 | return this.s3Api.getReport(params); 26 | } 27 | getCommitStatusOverview( 28 | params: CommitContext 29 | ): Promise { 30 | return this.s3Api.getCommitStatusOverview(params); 31 | } 32 | getAggregateReview(params: CommitContext): Promise { 33 | return this.s3Api.getAggregateReview(params); 34 | } 35 | approveReport(params: ReportContext): Promise { 36 | return this.restApi.approveReport(params); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/api/rest/config.ts: -------------------------------------------------------------------------------- 1 | import { GlobalEnvironmentSetup } from "@animaapp/scooby-shared"; 2 | import { APICreationOptions } from ".."; 3 | 4 | // These are injected automatically by Parcel: https://en.parceljs.org/env.html 5 | const DEFAULT_REST_API_BASE_URL = process.env.SCOOBY_SERVICE_BASE_URL; 6 | const DEFAULT_REST_API_ACCESS_TOKEN = process.env.SCOOBY_SERVICE_ACCESS_TOKEN; 7 | 8 | export type RestAPIConfig = { 9 | baseUrl: string; 10 | accessToken: string; 11 | environment: GlobalEnvironmentSetup; 12 | }; 13 | 14 | export function getRestAPIConfig(options: APICreationOptions): RestAPIConfig { 15 | const baseUrl = 16 | options.environment.restApi?.baseUrl ?? DEFAULT_REST_API_BASE_URL; 17 | const accessToken = 18 | options.environment.restApi?.accessToken ?? DEFAULT_REST_API_ACCESS_TOKEN; 19 | 20 | if (!baseUrl) { 21 | throw new Error("could not determine Rest API base URL"); 22 | } 23 | if (!accessToken) { 24 | throw new Error("could not determine Rest API access token"); 25 | } 26 | 27 | return { 28 | baseUrl, 29 | accessToken, 30 | environment: options.environment, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/api/rest/index.ts: -------------------------------------------------------------------------------- 1 | import { APICreationOptions } from ".."; 2 | import { ReportContext, ReviewScoobyWebAPI } from "../types"; 3 | import { getRestAPIConfig, RestAPIConfig } from "./config"; 4 | 5 | export class RestScoobyWebAPI implements ReviewScoobyWebAPI { 6 | private config: RestAPIConfig; 7 | 8 | constructor(options: APICreationOptions) { 9 | this.config = getRestAPIConfig(options); 10 | } 11 | 12 | async approveReport(params: ReportContext): Promise { 13 | await this.postAPIRequest("/api/review/approve", { 14 | environment: this.config.environment, 15 | repositoryName: params.repository, 16 | commitHash: params.commit, 17 | reports: [params.reportName], 18 | }); 19 | } 20 | 21 | async postAPIRequest(endpoint: string, body: unknown): Promise { 22 | const options: RequestInit = { 23 | method: "POST", 24 | headers: { 25 | "Content-Type": "application/json", 26 | Authorization: `Bearer ${this.config.accessToken}`, 27 | }, 28 | body: JSON.stringify(body), 29 | }; 30 | 31 | return fetch(`${this.config.baseUrl}${endpoint}`, options); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/api/s3/config.ts: -------------------------------------------------------------------------------- 1 | import { APICreationOptions } from ".."; 2 | 3 | export type S3Config = { 4 | region: string; 5 | bucket: string; 6 | }; 7 | 8 | export function getS3Config(options: APICreationOptions): S3Config { 9 | if (!options.environment.s3) { 10 | throw new Error("missing S3 configuration options"); 11 | } 12 | 13 | return { 14 | region: options.environment.s3.region, 15 | bucket: options.environment.s3.bucket, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/api/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommitStatusOverview, 3 | HostedReport, 4 | Review, 5 | } from "@animaapp/scooby-shared"; 6 | 7 | export type ReadScoobyWebAPI = { 8 | getReports(params: CommitContext): Promise; 9 | getReport(params: ReportContext): Promise; 10 | getCommitStatusOverview( 11 | params: CommitContext 12 | ): Promise; 13 | getAggregateReview(params: CommitContext): Promise; 14 | }; 15 | 16 | export type ReviewScoobyWebAPI = { 17 | approveReport(params: ReportContext): Promise; 18 | }; 19 | 20 | export type ScoobyWebAPI = ReadScoobyWebAPI & ReviewScoobyWebAPI; 21 | 22 | export type ReportId = string; 23 | 24 | export type CommitContext = { 25 | repository: string; 26 | commit: string; 27 | }; 28 | 29 | export type ReportContext = CommitContext & { 30 | reportName: string; 31 | }; 32 | 33 | export type APIRequest = keyof ScoobyWebAPI; 34 | export type APIRequestParams = Parameters< 35 | ScoobyWebAPI[TRequest] 36 | >[0]; 37 | export type APIResponse = Awaited< 38 | ReturnType 39 | >; 40 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/hooks/internal/mapping.ts: -------------------------------------------------------------------------------- 1 | import { QueryResponse } from "./useQuery"; 2 | 3 | export type MappedQueryResponse = { 4 | [K in TDataProperty]?: TData; 5 | } & Omit, "data">; 6 | 7 | export function remapQueryDataResponse( 8 | response: QueryResponse, 9 | as: TDataProperty 10 | ): MappedQueryResponse { 11 | // @ts-ignore 12 | return { 13 | ...omit(response, "data"), 14 | [as]: response.data, 15 | }; 16 | } 17 | 18 | function omit( 19 | obj: T, 20 | ...keys: K[] 21 | ): Omit { 22 | const _ = { ...obj }; 23 | keys.forEach((key) => delete _[key]); 24 | return _; 25 | } 26 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/hooks/useAggregateReview.ts: -------------------------------------------------------------------------------- 1 | import { CommitContext } from "../api"; 2 | import { remapQueryDataResponse } from "./internal/mapping"; 3 | import { useQuery } from "./internal/useQuery"; 4 | 5 | export function useAggregateReview(context: CommitContext) { 6 | const response = useQuery("getAggregateReview", context); 7 | 8 | return remapQueryDataResponse(response, "review"); 9 | } 10 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/hooks/useCommitStatusOverview.ts: -------------------------------------------------------------------------------- 1 | import { CommitContext } from "../api"; 2 | import { remapQueryDataResponse } from "./internal/mapping"; 3 | import { useQuery } from "./internal/useQuery"; 4 | 5 | export function useCommitStatusOverview(context: CommitContext) { 6 | const response = useQuery("getCommitStatusOverview", context); 7 | 8 | return remapQueryDataResponse(response, "overview"); 9 | } 10 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/hooks/useReport.ts: -------------------------------------------------------------------------------- 1 | import { ReportContext } from "../api"; 2 | import { remapQueryDataResponse } from "./internal/mapping"; 3 | import { useQuery } from "./internal/useQuery"; 4 | 5 | export function useReport(context: ReportContext) { 6 | const response = useQuery("getReport", context); 7 | 8 | return remapQueryDataResponse(response, "report"); 9 | } 10 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/hooks/useReportStatus.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { ReportContext } from "../api"; 3 | import { useCommitStatusOverview } from "./useCommitStatusOverview"; 4 | 5 | export function useReportStatus(context: ReportContext) { 6 | const { overview, isLoading, error } = useCommitStatusOverview({ 7 | commit: context.commit, 8 | repository: context.repository, 9 | }); 10 | 11 | const status = useMemo(() => { 12 | if (overview) { 13 | return overview.reports[context.reportName]; 14 | } 15 | }, [overview, context.reportName]); 16 | 17 | return { 18 | status, 19 | isLoading, 20 | error, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/hooks/useReports.ts: -------------------------------------------------------------------------------- 1 | import { CommitContext } from "../api"; 2 | import { remapQueryDataResponse } from "./internal/mapping"; 3 | import { useQuery } from "./internal/useQuery"; 4 | 5 | export function useReports(context: CommitContext) { 6 | const response = useQuery("getReports", context); 7 | 8 | return remapQueryDataResponse(response, "reports"); 9 | } 10 | -------------------------------------------------------------------------------- /packages/scooby-web/src/data-fetching/hooks/useURL.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | 3 | export function useURL(url: string | null): { 4 | source?: string; 5 | isLoading?: boolean; 6 | error?: unknown; 7 | } { 8 | const { data, error, isValidating } = useSWR(url, fetcher, { 9 | dedupingInterval: 120000, 10 | revalidateOnFocus: false, 11 | revalidateOnReconnect: false, 12 | }); 13 | 14 | return { 15 | source: data, 16 | error, 17 | isLoading: isValidating, 18 | }; 19 | } 20 | 21 | async function fetcher(url: string): Promise { 22 | const response = await fetch(url); 23 | 24 | return response.text(); 25 | } 26 | -------------------------------------------------------------------------------- /packages/scooby-web/src/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg"; 2 | declare module "*.png"; 3 | -------------------------------------------------------------------------------- /packages/scooby-web/src/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | height: 100%; 3 | } 4 | 5 | .ant-image-preview-img-wrapper { 6 | padding: 60px; 7 | } 8 | 9 | .clickable-list-item:hover { 10 | background-color: #a7d6f5 !important; 11 | cursor: pointer; 12 | } 13 | 14 | .split { 15 | display: flex; 16 | flex-direction: row; 17 | } 18 | 19 | .gutter { 20 | background-color: #eee; 21 | background-repeat: no-repeat; 22 | background-position: 50%; 23 | } 24 | 25 | .gutter.gutter-horizontal { 26 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=="); 27 | cursor: col-resize; 28 | } 29 | -------------------------------------------------------------------------------- /packages/scooby-web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 16 | 22 | 23 | 24 | 25 | Scooby 26 | 27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /packages/scooby-web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import { RouterProvider } from "react-router-dom"; 3 | import { APIContextProvider } from "./data-fetching/api/provider"; 4 | import { APICreationOptions, createAPI } from "./data-fetching/api"; 5 | 6 | import "antd/dist/antd.css"; 7 | import "./index.css"; 8 | import { 9 | GlobalEnvironmentSetup, 10 | parseGlobalEnvironmentSetup, 11 | } from "@animaapp/scooby-shared"; 12 | import { getSearchParams, router } from "./router"; 13 | 14 | function createAPIOptions(): APICreationOptions { 15 | const params = getSearchParams(); 16 | 17 | let environment: GlobalEnvironmentSetup = {}; 18 | 19 | // Legacy compatibility mode 20 | const region = params["_s3_region"]; 21 | const bucket = params["_s3_bucket"]; 22 | if (region && bucket) { 23 | environment.s3 = { 24 | region, 25 | bucket, 26 | }; 27 | } 28 | 29 | const serializedEnvironment = params["_env"]; 30 | if (serializedEnvironment) { 31 | environment = parseGlobalEnvironmentSetup( 32 | JSON.parse(atob(serializedEnvironment)) 33 | ); 34 | } 35 | 36 | return { 37 | environment, 38 | }; 39 | } 40 | 41 | const defaultApi = createAPI(createAPIOptions()); 42 | 43 | const app = document.getElementById("app"); 44 | ReactDOM.render( 45 | 46 | 47 | , 48 | app 49 | ); 50 | -------------------------------------------------------------------------------- /packages/scooby-web/src/providers/feedback/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useMemo, useRef } from "react"; 2 | import JSConfetti from "js-confetti"; 3 | 4 | interface FeedbackContextValue { 5 | confetti: () => void; 6 | } 7 | 8 | const FeedbackContext = createContext(null); 9 | 10 | export const FeedbackProvider: React.FC<{ 11 | children: React.ReactNode; 12 | }> = ({ children }) => { 13 | const confettiRef = useRef(new JSConfetti()); 14 | 15 | const context = useMemo( 16 | (): FeedbackContextValue => ({ 17 | confetti: () => { 18 | confettiRef.current.addConfetti({ 19 | confettiColors: [ 20 | "#1abc9c", 21 | "#f1c40f", 22 | "#f39c12", 23 | "#2ecc71", 24 | "#e74c3c", 25 | "#8e44ad", 26 | ], 27 | confettiRadius: 6, 28 | confettiNumber: 100, 29 | }); 30 | }, 31 | }), 32 | [] 33 | ); 34 | 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | }; 41 | 42 | export function useFeedback(): FeedbackContextValue { 43 | const appContext = useContext(FeedbackContext); 44 | 45 | if (!appContext) { 46 | throw new Error("uninitialized feedback context"); 47 | } 48 | 49 | return appContext; 50 | } 51 | -------------------------------------------------------------------------------- /packages/scooby-web/src/router.tsx: -------------------------------------------------------------------------------- 1 | import { createHashRouter } from "react-router-dom"; 2 | import Root from "./routes/Root"; 3 | import ErrorPage from "./routes/ErrorPage"; 4 | import Commit from "./routes/commit"; 5 | import Report from "./routes/report"; 6 | import Home from "./routes/Home"; 7 | 8 | export const router = createHashRouter([ 9 | { 10 | path: "/", 11 | element: , 12 | errorElement: , 13 | children: [ 14 | { 15 | path: "/", 16 | element: , 17 | }, 18 | { 19 | path: "repo/:repository/commit/:commit", 20 | element: , 21 | }, 22 | { 23 | path: "repo/:repository/commit/:commit/report/:reportName", 24 | element: , 25 | }, 26 | ], 27 | }, 28 | ]); 29 | 30 | export function getSearchParams(): Record { 31 | // We need to take the search parameters from the router state when using a hash router 32 | const urlSearchParams = new URLSearchParams(router.state.location.search); 33 | const params = Object.fromEntries(urlSearchParams.entries()); 34 | return params; 35 | } 36 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteError } from "react-router-dom"; 2 | 3 | export default function ErrorPage() { 4 | const error: any = useRouteError(); 5 | console.error(error); 6 | 7 | return ( 8 |
9 |

Oops!

10 |

Sorry, an unexpected error has occurred.

11 |

12 | {error.statusText || error.message} 13 |

14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/Home.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return ( 3 |
11 |

12 | Drop a ZIP report here to view it, or navigate directly to a Scooby 13 | report from a GitHub status. 14 |

15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/commit/Commit.tsx: -------------------------------------------------------------------------------- 1 | import { Breadcrumb, PageHeader } from "antd"; 2 | import { ReportId } from "../../data-fetching/api"; 3 | import { ReportList } from "./ReportList"; 4 | 5 | type Props = { 6 | reports: ReportId[]; 7 | repository: string; 8 | commit: string; 9 | }; 10 | 11 | export function Commit({ reports, repository, commit }: Props) { 12 | return ( 13 |
14 | 17 | {repository} 18 | {commit} 19 | 20 | } 21 | style={{ 22 | borderBottom: "1px solid #c9c9c9", 23 | marginBottom: "8px", 24 | }} 25 | /> 26 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/commit/CommitController.tsx: -------------------------------------------------------------------------------- 1 | import ErrorPanel from "../../components/ErrorPanel"; 2 | import Loader from "../../components/Loader"; 3 | import { useReports } from "../../data-fetching/hooks/useReports"; 4 | import { Commit } from "./Commit"; 5 | 6 | type Props = { 7 | commit: string; 8 | repository: string; 9 | }; 10 | 11 | export function CommitController({ commit, repository }: Props) { 12 | const { reports, isLoading, error } = useReports({ 13 | commit, 14 | repository, 15 | }); 16 | 17 | if (isLoading) { 18 | return ; 19 | } 20 | 21 | if (error) { 22 | return ; 23 | } 24 | 25 | if (!reports) { 26 | return ; 27 | } 28 | 29 | return ; 30 | } 31 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/commit/ReportList/ReportListItemController.tsx: -------------------------------------------------------------------------------- 1 | import { ReportId } from "../../../data-fetching/api"; 2 | import { useReport } from "../../../data-fetching/hooks/useReport"; 3 | import { useEnhancedNavigation } from "../../hooks/useEnhancedNavigation"; 4 | import { ReportListItem } from "./ReportListItem"; 5 | 6 | type Props = { 7 | reportId: ReportId; 8 | repository: string; 9 | commit: string; 10 | }; 11 | 12 | export const ReportListItemController = ({ 13 | reportId, 14 | repository, 15 | commit, 16 | }: Props) => { 17 | const { report, isLoading, error } = useReport({ 18 | commit, 19 | repository, 20 | reportName: reportId, 21 | }); 22 | 23 | const navigate = useEnhancedNavigation(); 24 | 25 | const handleReportSelection = (reportId: string) => { 26 | navigate("report/" + reportId); 27 | }; 28 | 29 | return ( 30 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/commit/ReportList/index.tsx: -------------------------------------------------------------------------------- 1 | import { List } from "antd"; 2 | import { ReportId } from "../../../data-fetching/api"; 3 | import { ReportListItemController } from "./ReportListItemController"; 4 | 5 | type Props = { 6 | reports: ReportId[]; 7 | repository: string; 8 | commit: string; 9 | }; 10 | 11 | export const ReportList = (props: Props) => { 12 | return ( 13 |
18 | ( 21 | 27 | )} 28 | /> 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/commit/index.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import ErrorPanel from "../../components/ErrorPanel"; 3 | import { CommitParams } from "../../types"; 4 | import { CommitController } from "./CommitController"; 5 | 6 | export default function CommitRoot() { 7 | const params = useParams(); 8 | 9 | if (!params.commit || !params.repository) { 10 | return ; 11 | } 12 | 13 | return ( 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/hooks/params.ts: -------------------------------------------------------------------------------- 1 | export function getProtectedParams( 2 | params: Record 3 | ): Record { 4 | const output: Record = {}; 5 | 6 | for (const [key, value] of Object.entries(params)) { 7 | if (key.startsWith("_")) { 8 | output[key] = value; 9 | } 10 | } 11 | 12 | return output; 13 | } 14 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/hooks/useEnhancedNavigation.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { NavigateFunction, useNavigate } from "react-router-dom"; 3 | import { getProtectedParams } from "./params"; 4 | import { useQueryParams } from "./useQueryParams"; 5 | 6 | // A useNavigate version that preserves global params 7 | export function useEnhancedNavigation(): NavigateFunction { 8 | const params = useQueryParams>(); 9 | const navigate = useNavigate(); 10 | 11 | const enhancedNavigate: NavigateFunction = useCallback( 12 | // @ts-ignore 13 | (to, options) => { 14 | const protectedParams = getProtectedParams(params); 15 | const serializedParams = new URLSearchParams(protectedParams).toString(); 16 | 17 | navigate(`${to}?${serializedParams}`, options); 18 | }, 19 | [params] 20 | ); 21 | 22 | return enhancedNavigate; 23 | } 24 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/hooks/useQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | 4 | export function useQueryParams(): T { 5 | const { search } = useLocation(); 6 | return useMemo(() => { 7 | const urlSearchParams = new URLSearchParams(search); 8 | 9 | return Object.fromEntries(urlSearchParams) as T; 10 | }, [search]); 11 | } 12 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/hooks/useUpdateParams.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { getProtectedParams } from "./params"; 4 | import { useQueryParams } from "./useQueryParams"; 5 | 6 | type Callback = 7 | | Record 8 | | ((oldParams: Record) => Record); 9 | 10 | export function useUpdateParams(): { 11 | updateParams: (callback: Callback, resetProtectedParams?: boolean) => void; 12 | } { 13 | const currentParams = useQueryParams>(); 14 | const navigate = useNavigate(); 15 | 16 | const updateParams = useCallback( 17 | (callback: Callback, resetProtectedParams = false) => { 18 | const oldParams = JSON.parse(JSON.stringify(currentParams)); 19 | let newParams = {}; 20 | if (typeof callback === "object") { 21 | newParams = callback; 22 | } else if (typeof callback === "function") { 23 | newParams = callback(oldParams); 24 | } 25 | const params = { 26 | ...(resetProtectedParams ? {} : getProtectedParams(oldParams)), 27 | ...newParams, 28 | }; 29 | 30 | const serializedParams = new URLSearchParams(params).toString(); 31 | navigate(`?${serializedParams}`); 32 | }, 33 | [currentParams] 34 | ); 35 | 36 | return { 37 | updateParams, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/ApproveButton/index.ts: -------------------------------------------------------------------------------- 1 | export { ApproveButtonController as ApproveButton } from "./ApproveButtonController"; 2 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/ReportController.tsx: -------------------------------------------------------------------------------- 1 | import ErrorPanel from "../../components/ErrorPanel"; 2 | import Loader from "../../components/Loader"; 3 | import { useAggregateReview } from "../../data-fetching/hooks/useAggregateReview"; 4 | import { useReport } from "../../data-fetching/hooks/useReport"; 5 | import { Report } from "./Report"; 6 | 7 | type Props = { 8 | commit: string; 9 | reportName: string; 10 | repository: string; 11 | }; 12 | 13 | export function ReportController({ commit, reportName, repository }: Props) { 14 | const { report, isLoading, error } = useReport({ 15 | commit, 16 | reportName, 17 | repository, 18 | }); 19 | 20 | const { review, isLoading: isReviewLoading } = useAggregateReview({ 21 | commit, 22 | repository, 23 | }); 24 | 25 | if (isLoading || isReviewLoading) { 26 | return ; 27 | } 28 | 29 | if (error) { 30 | return ; 31 | } 32 | 33 | if (!report) { 34 | return ; 35 | } 36 | 37 | return ( 38 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity-regression/actions.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from "../../../types"; 2 | 3 | export type SelectEntryAction = { type: "select-entry"; entry: Entry }; 4 | 5 | export type Action = SelectEntryAction; 6 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity-regression/comparison/ImageComparator/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ImageComparatorController as ImageComparator, 3 | ImageData, 4 | } from "./ImageComparatorController"; 5 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity-regression/comparison/index.tsx: -------------------------------------------------------------------------------- 1 | import { HostedFidelityRegressionReport } from "@animaapp/scooby-shared"; 2 | import ErrorPanel from "../../../../components/ErrorPanel"; 3 | import { ImageComparisonView } from "./ImageComparisonView"; 4 | 5 | type Props = { 6 | selectedId: string | undefined; 7 | report: HostedFidelityRegressionReport; 8 | }; 9 | 10 | export const ComparisonView = ({ selectedId, report }: Props) => { 11 | if (!selectedId) { 12 | return

Select an entry to get started

; 13 | } 14 | 15 | if (report.results.type === "image") { 16 | return ( 17 | 18 | ); 19 | } else if (report.results.type === "code") { 20 | return ( 21 |

22 | The Fidelity-Regression code view has not yet been implemented, please 23 | reach out to the Core team if you need it. 24 |

25 | ); 26 | } 27 | 28 | return ( 29 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity-regression/details/index.tsx: -------------------------------------------------------------------------------- 1 | import { HostedFidelityRegressionReport } from "@animaapp/scooby-shared"; 2 | import { DatabaseOutlined } from "@ant-design/icons"; 3 | import { Tabs } from "antd"; 4 | import { MetadataTab } from "./MetadataTab"; 5 | import "./style.css"; 6 | 7 | type Props = { 8 | selectedId: string | undefined; 9 | report: HostedFidelityRegressionReport; 10 | }; 11 | 12 | export const DetailsView = ({ selectedId, report }: Props) => { 13 | return ( 14 | 27 | 28 | Metadata 29 | 30 | ), 31 | children: , 32 | }, 33 | ]} 34 | /> 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity-regression/details/style.css: -------------------------------------------------------------------------------- 1 | #details-view .ant-tabs-content { 2 | height: 100%; 3 | } 4 | 5 | #details-view .ant-tabs-tabpane { 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity/FidelityReport.tsx: -------------------------------------------------------------------------------- 1 | import { HostedFidelityReport } from "@animaapp/scooby-shared"; 2 | import { EntryList } from "../../../components/EntryList"; 3 | import { VerticalSplitPane } from "../../../components/SplitPane"; 4 | import { Entry } from "../../../types"; 5 | import { Action } from "./actions"; 6 | import { ComparisonView } from "./comparison"; 7 | import { DetailsView } from "./details"; 8 | 9 | type Props = { 10 | report: HostedFidelityReport; 11 | entries: Entry[]; 12 | selectedId?: string; 13 | dispatchAction: (action: Action) => void; 14 | }; 15 | 16 | export function FidelityReport({ 17 | report, 18 | entries, 19 | selectedId, 20 | dispatchAction, 21 | }: Props) { 22 | const handleEntrySelected = (entry: Entry) => { 23 | dispatchAction({ type: "select-entry", entry }); 24 | }; 25 | 26 | return ( 27 |
28 | 35 | } 36 | center={} 37 | right={} 38 | /> 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity/actions.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from "../../../types"; 2 | 3 | export type SelectEntryAction = { type: "select-entry"; entry: Entry }; 4 | 5 | export type Action = SelectEntryAction; 6 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity/comparison/CodeComparisonView.tsx: -------------------------------------------------------------------------------- 1 | import { HostedResource, CodeFidelityTestPair } from "@animaapp/scooby-shared"; 2 | import { useMemo } from "react"; 3 | import { CodeComparator } from "../../../../components/CodeComparator"; 4 | 5 | type Props = { 6 | pair: CodeFidelityTestPair; 7 | }; 8 | 9 | export const CodeComparisonView = ({ pair }: Props) => { 10 | const codeData = useMemo( 11 | () => 12 | ({ 13 | type: "pair", 14 | similarity: pair.comparison.similarity, 15 | actualUrl: pair.actual.code.url, 16 | expectedUrl: pair.expected.code.url, 17 | rawDiffUrl: pair.comparison.diff?.url, 18 | filePath: pair.actual.path, 19 | } as const), 20 | [pair] 21 | ); 22 | 23 | return ; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity/comparison/ImageComparisonView.tsx: -------------------------------------------------------------------------------- 1 | import { HostedResource, ImageFidelityTestPair } from "@animaapp/scooby-shared"; 2 | import { useMemo } from "react"; 3 | import { ImageComparator } from "../../../../components/ImageComparator"; 4 | 5 | type Props = { 6 | pair: ImageFidelityTestPair; 7 | }; 8 | 9 | export const ImageComparisonView = ({ pair }: Props) => { 10 | const imageData = useMemo( 11 | () => 12 | ({ 13 | type: "pair", 14 | actualUrl: pair.actual.image.url, 15 | expectedUrl: pair.expected.image.url, 16 | diffUrl: pair.comparison.diff.url, 17 | overlapUrl: pair.comparison.overlap.url, 18 | similarity: pair.comparison.similarity, 19 | } as const), 20 | [pair] 21 | ); 22 | 23 | return ; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity/details/MetadataTab.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | HostedFidelityReport, 3 | HostedResource, 4 | Metadata, 5 | } from "@animaapp/scooby-shared"; 6 | import { useMemo } from "react"; 7 | import { MetadataList } from "../../../../components/MetadataList"; 8 | 9 | type Props = { 10 | selectedId: string | undefined; 11 | report: HostedFidelityReport; 12 | }; 13 | 14 | export const MetadataTab = ({ selectedId, report }: Props) => { 15 | const metadata: Metadata[] = useMemo(() => { 16 | if (!selectedId) { 17 | return []; 18 | } 19 | 20 | for (const entry of report.pairs) { 21 | if (entry.actual.id === selectedId) { 22 | return [ 23 | ...(entry.actual.metadata?.map((metadataEntry) => ({ 24 | ...metadataEntry, 25 | name: `(actual) ${metadataEntry.name}`, 26 | })) ?? []), 27 | ...(entry.expected.metadata?.map((metadataEntry) => ({ 28 | ...metadataEntry, 29 | name: `(expected) ${metadataEntry.name}`, 30 | })) ?? []), 31 | ]; 32 | } 33 | } 34 | 35 | return []; 36 | }, [report, selectedId]); 37 | 38 | return ; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity/details/index.tsx: -------------------------------------------------------------------------------- 1 | import { HostedFidelityReport } from "@animaapp/scooby-shared"; 2 | import { DatabaseOutlined } from "@ant-design/icons"; 3 | import { Tabs } from "antd"; 4 | import { MetadataTab } from "./MetadataTab"; 5 | import "./style.css"; 6 | 7 | type Props = { 8 | selectedId: string | undefined; 9 | report: HostedFidelityReport; 10 | }; 11 | 12 | export const DetailsView = ({ selectedId, report }: Props) => { 13 | return ( 14 | 27 | 28 | Metadata 29 | 30 | ), 31 | children: , 32 | }, 33 | ]} 34 | /> 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/fidelity/details/style.css: -------------------------------------------------------------------------------- 1 | #details-view .ant-tabs-content { 2 | height: 100%; 3 | } 4 | 5 | #details-view .ant-tabs-tabpane { 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/index.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import ErrorPanel from "../../components/ErrorPanel"; 3 | import { ReportParams } from "../../types"; 4 | import { ReportController } from "./ReportController"; 5 | 6 | export default function ReportRoot() { 7 | const params = useParams(); 8 | 9 | if (!params.commit || !params.reportName || !params.repository) { 10 | return ; 11 | } 12 | 13 | return ( 14 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/regression/RegressionReport.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | RegressionReport, 3 | HostedRegressionReport, 4 | } from "@animaapp/scooby-shared"; 5 | import { EntryList } from "../../../components/EntryList"; 6 | import { VerticalSplitPane } from "../../../components/SplitPane"; 7 | import { Entry } from "../../../types"; 8 | import { Action } from "./actions"; 9 | import { ComparisonView } from "./comparison"; 10 | import { DetailsView } from "./details"; 11 | 12 | type Props = { 13 | report: HostedRegressionReport; 14 | entries: Entry[]; 15 | selectedId?: string; 16 | dispatchAction: (action: Action) => void; 17 | }; 18 | 19 | export function RegressionReport({ 20 | report, 21 | entries, 22 | selectedId, 23 | dispatchAction, 24 | }: Props) { 25 | const handleEntrySelected = (entry: Entry) => { 26 | dispatchAction({ type: "select-entry", entry }); 27 | }; 28 | 29 | return ( 30 |
31 | 38 | } 39 | center={} 40 | right={} 41 | /> 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/regression/actions.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from "../../../types"; 2 | 3 | export type SelectEntryAction = { type: "select-entry"; entry: Entry }; 4 | 5 | export type Action = SelectEntryAction; 6 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/regression/comparison/index.tsx: -------------------------------------------------------------------------------- 1 | import { HostedRegressionReport } from "@animaapp/scooby-shared"; 2 | import ErrorPanel from "../../../../components/ErrorPanel"; 3 | import { CodeComparisonView } from "./CodeComparisonView"; 4 | import { ImageComparisonView } from "./ImageComparisonView"; 5 | 6 | type Props = { 7 | selectedId: string | undefined; 8 | report: HostedRegressionReport; 9 | }; 10 | 11 | export const ComparisonView = ({ selectedId, report }: Props) => { 12 | if (!selectedId) { 13 | return

Select an entry to get started

; 14 | } 15 | 16 | if (report.results.type === "image") { 17 | return ( 18 | 19 | ); 20 | } else if (report.results.type === "code") { 21 | return ( 22 | 23 | ); 24 | } 25 | 26 | return ( 27 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/regression/details/index.tsx: -------------------------------------------------------------------------------- 1 | import { HostedRegressionReport } from "@animaapp/scooby-shared"; 2 | import { DatabaseOutlined } from "@ant-design/icons"; 3 | import { Tabs } from "antd"; 4 | import { MetadataTab } from "./MetadataTab"; 5 | import "./style.css"; 6 | 7 | type Props = { 8 | selectedId: string | undefined; 9 | report: HostedRegressionReport; 10 | }; 11 | 12 | export const DetailsView = ({ selectedId, report }: Props) => { 13 | return ( 14 | 27 | 28 | Metadata 29 | 30 | ), 31 | children: , 32 | }, 33 | ]} 34 | /> 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/scooby-web/src/routes/report/regression/details/style.css: -------------------------------------------------------------------------------- 1 | #details-view .ant-tabs-content { 2 | height: 100%; 3 | } 4 | 5 | #details-view .ant-tabs-tabpane { 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /packages/scooby-web/src/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-web/src/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/scooby-web/src/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-web/src/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/scooby-web/src/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-web/src/static/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/scooby-web/src/static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/scooby-web/src/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-web/src/static/favicon-16x16.png -------------------------------------------------------------------------------- /packages/scooby-web/src/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-web/src/static/favicon-32x32.png -------------------------------------------------------------------------------- /packages/scooby-web/src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-web/src/static/favicon.ico -------------------------------------------------------------------------------- /packages/scooby-web/src/static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/packages/scooby-web/src/static/mstile-150x150.png -------------------------------------------------------------------------------- /packages/scooby-web/src/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /packages/scooby-web/src/types.ts: -------------------------------------------------------------------------------- 1 | // ROUTE PARAMS 2 | 3 | import { Sentiment } from "@animaapp/scooby-shared"; 4 | 5 | export type CommitParams = { 6 | repository: string; 7 | commit: string; 8 | }; 9 | 10 | export type ReportParams = CommitParams & { 11 | reportName: string; 12 | }; 13 | 14 | export type BaseEntry = { 15 | id: string; 16 | sentiment?: Sentiment; 17 | tag?: string; 18 | status?: EntryStatus; 19 | path?: string; 20 | score?: number; 21 | }; 22 | 23 | export type EntryStatus = "approved" | "changes_requested"; 24 | 25 | export type ImageEntry = BaseEntry & { 26 | type: "image"; 27 | thumbnailUrl: string; 28 | }; 29 | 30 | export type CodeEntry = BaseEntry & { 31 | type: "code"; 32 | }; 33 | 34 | export type Entry = ImageEntry | CodeEntry; 35 | -------------------------------------------------------------------------------- /packages/scooby-web/src/utils/capitalize.ts: -------------------------------------------------------------------------------- 1 | export function capitalize(string: string): string { 2 | return string.charAt(0).toUpperCase() + string.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /packages/scooby-web/src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | import { Sentiment } from "@animaapp/scooby-shared"; 2 | 3 | export function getColorForSentiment(sentiment: Sentiment): string { 4 | switch (sentiment) { 5 | case "danger": 6 | return "red"; 7 | case "info": 8 | return "blue"; 9 | case "success": 10 | return "green"; 11 | case "warning": 12 | return "orange"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/scooby-web/src/utils/language.ts: -------------------------------------------------------------------------------- 1 | export function getLanguageForFile(path: string): string | undefined { 2 | const tokens = path.split("."); 3 | if (tokens.length < 2) { 4 | return; 5 | } 6 | 7 | const extension = tokens[tokens.length - 1].toLowerCase(); 8 | 9 | switch (extension) { 10 | case "json": 11 | return "json"; 12 | case "html": 13 | return "html"; 14 | case "jsx": 15 | case "js": 16 | return "javascript"; 17 | case "ts": 18 | case "tsx": 19 | return "typescript"; 20 | case "css": 21 | return "css"; 22 | case "less": 23 | return "less"; 24 | case "scss": 25 | return "scss"; 26 | case "sass": 27 | return "sass"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/scooby-web/src/utils/rank.ts: -------------------------------------------------------------------------------- 1 | import { Sentiment } from "@animaapp/scooby-shared"; 2 | 3 | export function getRankForSentiment(sentiment: Sentiment): number { 4 | switch (sentiment) { 5 | case "danger": 6 | return 0; 7 | case "warning": 8 | return 1; 9 | case "info": 10 | return 2; 11 | case "success": 12 | return 3; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/scooby-web/src/utils/search.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from "../types"; 2 | 3 | export function isEntryMatchingQuery(entry: Entry, query: string): boolean { 4 | const tokens = query.split(" "); 5 | 6 | for (const token of tokens) { 7 | if ( 8 | !( 9 | entry.id.toLowerCase().includes(token) || 10 | entry.path?.toLowerCase().includes(token) 11 | ) 12 | ) { 13 | return false; 14 | } 15 | } 16 | 17 | return true; 18 | } 19 | -------------------------------------------------------------------------------- /packages/scooby-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "jsx": "react-jsx", 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "strict": true, 11 | "allowSyntheticDefaultImports": true, 12 | "skipLibCheck": true, 13 | "paths": { 14 | "@animaapp/scooby-shared": ["../scooby-shared"] 15 | } 16 | }, 17 | "exclude": ["node_modules", "__tests__", "dist"], 18 | "references": [{ "path": "../scooby-shared" }] 19 | } 20 | -------------------------------------------------------------------------------- /sample-projects/basic-fidelity/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/basic-fidelity/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/basic-fidelity/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test3

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/basic-fidelity/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/basic-fidelity/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 |

This is a difference in fidelity

10 | 11 | 12 | -------------------------------------------------------------------------------- /sample-projects/basic-fidelity/expected/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test3

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/basic-regression/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 |

This has been changed

10 | 11 | -------------------------------------------------------------------------------- /sample-projects/basic-regression/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/basic-regression/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

This is a new test

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/basic-regression/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/basic-regression/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/basic-regression/expected/this-has-been-removed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/ci-html-code-regression/test1.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 30px; 3 | } -------------------------------------------------------------------------------- /sample-projects/ci-html-code-regression/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/ci-html-code-regression/test2.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 30px; 3 | } -------------------------------------------------------------------------------- /sample-projects/ci-html-code-regression/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/ci-html-code-regression/test3.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 30px; 3 | } -------------------------------------------------------------------------------- /sample-projects/ci-html-code-regression/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/actual/test1-image.scooby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-html-fidelity-metadata/actual/test1-image.scooby.png -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/actual/test1.code.scooby.jsx: -------------------------------------------------------------------------------- 1 | function Goodbye(props) { 2 | return

Goodbye, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/actual/test1.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "type": "text", 5 | "name": "Example text", 6 | "text": "text metadata" 7 | }, 8 | { 9 | "type": "link", 10 | "name": "Example link", 11 | "description": "it even has a description", 12 | "url": "https://google.com" 13 | }, 14 | { 15 | "type": "image", 16 | "name": "Example image", 17 | "image_path": "test1-image.scooby.png" 18 | }, 19 | { 20 | "type": "file", 21 | "name": "Example file", 22 | "file_path": "./test1-image.scooby.png" 23 | }, 24 | { 25 | "type": "code", 26 | "name": "Example code", 27 | "code_path": "./test1.code.scooby.jsx" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/actual/test2.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "type": "text", 5 | "name": "Example text", 6 | "text": "another text metadata" 7 | }, 8 | { 9 | "type": "link", 10 | "name": "Example link", 11 | "description": "it even has a description", 12 | "url": "https://google.com" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test3

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 |

This is a difference in fidelity

10 | 11 | 12 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/expected/test2.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "type": "text", 5 | "name": "Example text", 6 | "text": "expected text metadata" 7 | }, 8 | { 9 | "type": "link", 10 | "name": "Example link", 11 | "description": "it even has a description", 12 | "url": "https://google.com" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-metadata/expected/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test3

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression-shared-images/actual/1234-html/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression-shared-images/actual/1234-react/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression-shared-images/expected/1234/Test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-html-fidelity-regression-shared-images/expected/1234/Test1.png -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2 (this is wrong)

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression/expected/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression/reference/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression/reference/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2 (this is wrong)

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity-regression/reference/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3 (this was wrong)

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test3

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity/expected/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity/expected/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 |

This is a difference in fidelity

10 | 11 | 12 | -------------------------------------------------------------------------------- /sample-projects/ci-html-fidelity/expected/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test3

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-html-regression/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/ci-html-regression/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/ci-html-regression/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 3

9 | 10 | -------------------------------------------------------------------------------- /sample-projects/ci-json-fidelity/actual/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": false 3 | } -------------------------------------------------------------------------------- /sample-projects/ci-json-fidelity/actual/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "changed", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /sample-projects/ci-json-fidelity/expected/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": true 3 | } -------------------------------------------------------------------------------- /sample-projects/ci-json-fidelity/expected/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "test", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /sample-projects/ci-json-regression-metadata/test1-image.scooby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-json-regression-metadata/test1-image.scooby.png -------------------------------------------------------------------------------- /sample-projects/ci-json-regression-metadata/test1.code.scooby.jsx: -------------------------------------------------------------------------------- 1 | function Goodbye(props) { 2 | return

Goodbye, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-json-regression-metadata/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": false 3 | } -------------------------------------------------------------------------------- /sample-projects/ci-json-regression-metadata/test1.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "type": "text", 5 | "name": "Example text", 6 | "text": "text metadata" 7 | }, 8 | { 9 | "type": "link", 10 | "name": "Example link", 11 | "description": "it even has a description", 12 | "url": "https://google.com" 13 | }, 14 | { 15 | "type": "image", 16 | "name": "Example image", 17 | "image_path": "test1-image.scooby.png" 18 | }, 19 | { 20 | "type": "file", 21 | "name": "Example file", 22 | "file_path": "./test1-image.scooby.png" 23 | }, 24 | { 25 | "type": "code", 26 | "name": "Example code", 27 | "code_path": "./test1.code.scooby.jsx" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /sample-projects/ci-json-regression-metadata/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "changed", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /sample-projects/ci-json-regression-metadata/test2.scooby.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "type": "text", 5 | "name": "Example text", 6 | "text": "another text metadata" 7 | }, 8 | { 9 | "type": "link", 10 | "name": "Example link", 11 | "description": "it even has a description", 12 | "url": "https://google.com" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /sample-projects/ci-json-regression/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": false 3 | } -------------------------------------------------------------------------------- /sample-projects/ci-json-regression/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "another": "changed", 3 | "name": "test2" 4 | } -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity-mixed/actual/test1.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity-mixed/actual/test1.jsx: -------------------------------------------------------------------------------- 1 | function Goodbye(props) { 2 | return

Goodbye, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity-mixed/actual/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity-mixed/expected/test1.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity-mixed/expected/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity-mixed/expected/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity/actual/test1.jsx: -------------------------------------------------------------------------------- 1 | function Goodbye(props) { 2 | return

Goodbye, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity/actual/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity/expected/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-fidelity/expected/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-regression-mixed/test1.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-regression-mixed/test1.jsx: -------------------------------------------------------------------------------- 1 | function Goodbye(props) { 2 | return

Goodbye, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-regression-mixed/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-regression/test1.jsx: -------------------------------------------------------------------------------- 1 | function Goodbye(props) { 2 | return

Goodbye, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-jsx-regression/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sample-projects/ci-mixed-fidelity/actual/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 1

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-mixed-fidelity/actual/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test 2

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-mixed-fidelity/actual/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Template 6 | 7 | 8 |

Test3

9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-projects/ci-mixed-fidelity/expected/test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-mixed-fidelity/expected/test1.png -------------------------------------------------------------------------------- /sample-projects/ci-mixed-fidelity/expected/test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-mixed-fidelity/expected/test2.png -------------------------------------------------------------------------------- /sample-projects/ci-mixed-fidelity/expected/test3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-mixed-fidelity/expected/test3.png -------------------------------------------------------------------------------- /sample-projects/ci-nested-jsx-fidelity/actual/project1/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-nested-jsx-fidelity/actual/project1/test2.jsx: -------------------------------------------------------------------------------- 1 | function Different(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sample-projects/ci-nested-jsx-fidelity/actual/project2/test1.jsx: -------------------------------------------------------------------------------- 1 | function AnotherProject(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-nested-jsx-fidelity/expected/project1/test1.jsx: -------------------------------------------------------------------------------- 1 | function Welcome(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-nested-jsx-fidelity/expected/project1/test2.jsx: -------------------------------------------------------------------------------- 1 | function Comment(props) { 2 | return ( 3 |
4 |
5 | {props.author.name} 10 |
{props.author.name}
11 |
12 |
{props.text}
13 |
{formatDate(props.date)}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sample-projects/ci-nested-jsx-fidelity/expected/project2/test1.jsx: -------------------------------------------------------------------------------- 1 | function AnotherProject(props) { 2 | return

Hello, {props.name}

; 3 | } 4 | -------------------------------------------------------------------------------- /sample-projects/ci-png-regression/form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-png-regression/form.png -------------------------------------------------------------------------------- /sample-projects/ci-png-regression/menu-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-png-regression/menu-horizontal.png -------------------------------------------------------------------------------- /sample-projects/ci-png-regression/menu-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-png-regression/menu-vertical.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/html/img/ellipse-2-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/html/img/ellipse-2-1@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/html/img/ellipse-2-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/html/img/ellipse-2-2@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/html/img/rectangle-4-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/html/img/rectangle-4-1@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/html/img/rectangle-4-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/html/img/rectangle-4-2@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/html/img/rectangle-7-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/html/img/rectangle-7-1@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/html/img/rectangle-7-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/html/img/rectangle-7-2@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/react/img/ellipse-2-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/react/img/ellipse-2-1@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/react/img/ellipse-2-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/react/img/ellipse-2-2@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/react/img/rectangle-4-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/react/img/rectangle-4-1@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/react/img/rectangle-4-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/react/img/rectangle-4-2@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/react/img/rectangle-7-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/react/img/rectangle-7-1@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/react/img/rectangle-7-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/react/img/rectangle-7-2@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/react/index.html: -------------------------------------------------------------------------------- 1 | Anima React Package
-------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/vue/img/ellipse-2-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/vue/img/ellipse-2-1@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/vue/img/ellipse-2-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/vue/img/ellipse-2-2@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/vue/img/rectangle-4-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/vue/img/rectangle-4-1@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/vue/img/rectangle-4-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/vue/img/rectangle-4-2@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/vue/img/rectangle-7-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/vue/img/rectangle-7-1@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/vue/img/rectangle-7-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnimaApp/scooby/87c321602d769c53e58667a2ab17767f1b828352/sample-projects/ci-recursive-regression/vue/img/rectangle-7-2@2x.png -------------------------------------------------------------------------------- /sample-projects/ci-recursive-regression/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Anima Vue Package 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /scripts/publish_to_public_npm_registry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Patching NPM registry to point to public NPM registry" 6 | 7 | git grep -rl 'https://npm.pkg.github.com' packages/ > affected_files.txt 8 | 9 | echo "These files will be affected" 10 | cat affected_files.txt 11 | 12 | if [ "$(uname)" == "Darwin" ]; then 13 | # macOS 14 | cat affected_files.txt | xargs sed -i '' 's,https://npm.pkg.github.com,https://registry.npmjs.org,g' 15 | else 16 | # Linux 17 | cat affected_files.txt | xargs sed -i 's,https://npm.pkg.github.com,https://registry.npmjs.org,g' 18 | fi 19 | 20 | echo "Publishing packages" 21 | 22 | for package in `ls packages`; do 23 | PACKAGE_PATH=packages/$package 24 | pushd $PACKAGE_PATH 25 | 26 | yarn publish --access public 27 | 28 | popd 29 | done -------------------------------------------------------------------------------- /scripts/remove_unnecessary_packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Removing unnecessary packages" 4 | 5 | # We move the ones we want to keep 6 | mkdir pkg_to_keep 7 | 8 | mv packages/scooby-service pkg_to_keep 9 | mv packages/scooby-core pkg_to_keep 10 | mv packages/scooby-context pkg_to_keep 11 | mv packages/scooby-github-api pkg_to_keep 12 | mv packages/scooby-api pkg_to_keep 13 | mv packages/scooby-shared pkg_to_keep 14 | 15 | # Delete all the others 16 | rm -Rf packages/ 17 | 18 | # And replace it back 19 | mv pkg_to_keep packages 20 | 21 | ls -la packages/ -------------------------------------------------------------------------------- /scripts/run_all_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Running all package tests (when defined)" 6 | 7 | # We only want to run tests for the packages that have a "test" script defined 8 | 9 | for package in packages/* ; do 10 | package_test_command=$(cat "$package/package.json" | jq -r '.scripts.test') 11 | package_name=$(cat "$package/package.json" | jq -r '.name') 12 | if [ "$package_test_command" != "null" ] ; then 13 | echo "Testing package $package_name" 14 | npx yarn workspace "$package_name" test 15 | else 16 | echo "Skipping package $package_name, no tests defined" 17 | fi 18 | done --------------------------------------------------------------------------------