├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .prettierrc ├── .storybook ├── .babelrc ├── addons.js ├── main.js ├── middleware.js ├── playwright-favourite-actions.json ├── preview.js ├── setup-playwright.js └── webpack.config.js ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── __manual_mocks__ ├── hooks │ ├── use-active-browser.ts │ ├── use-current-story-action-sets.ts │ ├── use-global-action-dispatch.ts │ ├── use-global-screenshot-dispatch.ts │ ├── use-screenshot.ts │ └── use-story-action-sets-loader.ts ├── jest-image-snapshot.ts ├── playwright │ ├── index.ts │ └── pageProps.ts ├── react-useEffect.ts ├── store │ ├── action │ │ └── context.ts │ └── screenshot │ │ └── context.ts ├── ts-to-json.ts └── utils │ └── load-story-data.ts ├── __mocks__ ├── @storybook │ ├── addons.ts │ └── api.ts ├── fast-glob.ts ├── fix-screenshot-file-name.ts ├── fs.ts ├── get-story-playwright-data-by-file-name.ts ├── get-story-playwright-file-info.ts ├── jest-image-snapshot.ts ├── nanoid.ts ├── notistack.ts ├── react-use │ └── lib │ │ ├── useKey.ts │ │ ├── useMouseHovered.ts │ │ └── useThrottleFn.ts ├── reinspect.ts ├── release-modifier-Key.ts ├── touch.ts ├── use-fix-screenshot-file-name.ts └── use-key-press-fn.ts ├── __test_data__ ├── action-schema.ts ├── assets │ └── test-image-snap.png ├── configs.ts ├── get-org-editing-actionSet.ts ├── get-screenshot-date.ts ├── index.ts ├── story-data.ts ├── story-file-info.ts └── storybook-state.ts ├── assets └── addon-screenshot.gif ├── babel.config.js ├── configs.d.ts ├── configs.js ├── jest.config.js ├── middleware.d.ts ├── middleware.js ├── package.json ├── preset.js ├── register.js ├── setupTests.ts ├── src ├── __tests__ │ ├── get-screenshots.test.ts │ ├── register.test.tsx │ ├── run-image-diff.test.ts │ └── to-match-screenshots.test.ts ├── api │ ├── client │ │ ├── __mocks__ │ │ │ ├── delete-action-set.ts │ │ │ ├── test-screenshot.ts │ │ │ ├── test-screenshots.ts │ │ │ └── update-screenshot.ts │ │ ├── __tests__ │ │ │ ├── add-favourite-action.test.ts │ │ │ ├── change-screenshot-index.test.ts │ │ │ ├── delete-action-set.test.ts │ │ │ ├── delete-favourite-action.test.ts │ │ │ ├── delete-screenshot.test.ts │ │ │ ├── delete-story-screenshots.test.ts │ │ │ ├── fix-screenshot-file-name.test.ts │ │ │ ├── get-action-schema.test.ts │ │ │ ├── get-action-set.test.ts │ │ │ ├── get-favourite-actions.test.ts │ │ │ ├── get-schema-client.test.ts │ │ │ ├── get-screenshot.test.ts │ │ │ ├── get-story-screenshots.test.ts │ │ │ ├── get-theme-data.ts │ │ │ ├── save-action-set.test.ts │ │ │ ├── save-screenshot.test.ts │ │ │ ├── test-screenshot.test.ts │ │ │ ├── test-screenshots.test.ts │ │ │ ├── test-story-screenshots.test.ts │ │ │ └── update-screenshot.test.ts │ │ ├── add-favourite-action.ts │ │ ├── change-screenshot-index.ts │ │ ├── delete-action-set.ts │ │ ├── delete-favourite-action.ts │ │ ├── delete-screenshot.ts │ │ ├── delete-story-screenshots.ts │ │ ├── fix-screenshot-file-name.ts │ │ ├── get-action-schema.ts │ │ ├── get-action-set.ts │ │ ├── get-favourite-actions.ts │ │ ├── get-schema-client.ts │ │ ├── get-screenshot.ts │ │ ├── get-story-screenshots.ts │ │ ├── get-theme-data.ts │ │ ├── index.ts │ │ ├── save-action-set.ts │ │ ├── save-screenshot.ts │ │ ├── test-screenshot.ts │ │ ├── test-screenshots.ts │ │ ├── test-story-screenshots.ts │ │ ├── update-screenshot.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ ├── endpoint.test.ts │ │ │ └── response-handler.test.ts │ │ │ ├── endpoint.ts │ │ │ ├── index.ts │ │ │ └── response-handler.ts │ ├── server │ │ ├── __mocks__ │ │ │ └── configs.ts │ │ ├── __tests__ │ │ │ ├── configs.test.ts │ │ │ └── routes.test.ts │ │ ├── configs.ts │ │ ├── controller │ │ │ ├── __tests__ │ │ │ │ ├── add-to-favourite.test.ts │ │ │ │ ├── change-screenshot-index.test.ts │ │ │ │ ├── delete-action-set.test.ts │ │ │ │ ├── delete-favourite-action.test.ts │ │ │ │ ├── delete-screenshot.test.ts │ │ │ │ ├── delete-story-screenshots.test.ts │ │ │ │ ├── fix-screenshot-file-name.test.ts │ │ │ │ ├── get-action-set.test.ts │ │ │ │ ├── get-actions-schema.test.ts │ │ │ │ ├── get-favourite-actions.test.ts │ │ │ │ ├── get-schema-controller.test.ts │ │ │ │ ├── get-screenshot.test.ts │ │ │ │ ├── get-story-screenshots.test.ts │ │ │ │ ├── get-theme-data.test.ts │ │ │ │ ├── save-action-set.test.ts │ │ │ │ ├── save-screenshot.test.ts │ │ │ │ ├── test-screenshot.test.ts │ │ │ │ ├── test-screenshots.test.ts │ │ │ │ ├── test-story-screenshots.test.ts │ │ │ │ └── update-screenshot.test.ts │ │ │ ├── add-to-favourite.ts │ │ │ ├── change-screenshot-index.ts │ │ │ ├── delete-action-set.ts │ │ │ ├── delete-favourite-action.ts │ │ │ ├── delete-screenshot.ts │ │ │ ├── delete-story-screenshots.ts │ │ │ ├── fix-screenshot-file-name.ts │ │ │ ├── get-action-set.ts │ │ │ ├── get-actions-schema.ts │ │ │ ├── get-favourite-actions.ts │ │ │ ├── get-schema-controller.ts │ │ │ ├── get-screenshot.ts │ │ │ ├── get-story-screenshots.ts │ │ │ ├── get-theme-data.ts │ │ │ ├── index.ts │ │ │ ├── save-action-set.ts │ │ │ ├── save-screenshot.ts │ │ │ ├── test-screenshot.ts │ │ │ ├── test-screenshots.ts │ │ │ ├── test-story-screenshots.ts │ │ │ └── update-screenshot.ts │ │ ├── data │ │ │ ├── action-schema.json │ │ │ ├── browser-option-schema.json │ │ │ └── screenshot-option-schema.json │ │ ├── migration │ │ │ ├── __mocks__ │ │ │ │ └── migration.ts │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── migration.test.ts.snap │ │ │ │ ├── migration-v0-data.json │ │ │ │ ├── migration-v1-data.json │ │ │ │ └── migration.test.ts │ │ │ ├── index.ts │ │ │ ├── migration-v1.ts │ │ │ ├── migration-v2.ts │ │ │ ├── migration-v3.ts │ │ │ ├── migration-v4.ts │ │ │ └── migration.ts │ │ ├── routes.ts │ │ ├── scripts │ │ │ ├── __tests__ │ │ │ │ ├── generate-all-schema.test.ts │ │ │ │ └── generate-schema.test.ts │ │ │ ├── generate-action-schema.ts │ │ │ ├── generate-all-schema.ts │ │ │ └── generate-schema.ts │ │ ├── services │ │ │ ├── __mocks__ │ │ │ │ ├── change-screenshot-index.ts │ │ │ │ ├── delete-action-set.ts │ │ │ │ ├── delete-screenshot.ts │ │ │ │ ├── delete-story-screenshots.ts │ │ │ │ ├── diff-image-to-screenshot.ts │ │ │ │ ├── get-action-set.ts │ │ │ │ ├── get-actions-schema.ts │ │ │ │ ├── get-story-screenshots-data.ts │ │ │ │ ├── get-theme-data.ts │ │ │ │ ├── make-screenshot.ts │ │ │ │ ├── save-action-set.ts │ │ │ │ ├── save-screenshot.ts │ │ │ │ ├── test-screenshot-service.ts │ │ │ │ ├── test-screenshots-service.ts │ │ │ │ ├── test-story-screenshots.ts │ │ │ │ └── update-screenshot-service.ts │ │ │ ├── __tests__ │ │ │ │ ├── add-to-favourite.test.ts │ │ │ │ ├── change-screenshot-index.test.ts │ │ │ │ ├── delete-action-set.test.ts │ │ │ │ ├── delete-favourite-action.test.ts │ │ │ │ ├── delete-screenshot.test.ts │ │ │ │ ├── delete-story-screenshots.test.ts │ │ │ │ ├── diff-image-to-screenshot.test.ts │ │ │ │ ├── fix-screenshot-file-name.test.ts │ │ │ │ ├── get-action-set.test.ts │ │ │ │ ├── get-actions-schema.test.ts │ │ │ │ ├── get-favourite-actions.test.ts │ │ │ │ ├── get-schema-service.test.ts │ │ │ │ ├── get-story-screenshots-data.test.ts │ │ │ │ ├── get-theme-data.test.ts │ │ │ │ ├── make-screenshot.test.ts │ │ │ │ ├── save-action-set.test.ts │ │ │ │ ├── save-screenshot.test.ts │ │ │ │ ├── test-file-screenshots.test.ts │ │ │ │ ├── test-screenshot-service.test.ts │ │ │ │ ├── test-screenshots-service.test.ts │ │ │ │ ├── test-story-screenshots.test.ts │ │ │ │ └── update-screenshot-service.test.ts │ │ │ ├── add-to-favourite.ts │ │ │ ├── change-screenshot-index.ts │ │ │ ├── constants │ │ │ │ ├── common.ts │ │ │ │ └── index.ts │ │ │ ├── delete-action-set.ts │ │ │ ├── delete-favourite-action.ts │ │ │ ├── delete-screenshot.ts │ │ │ ├── delete-story-screenshots.ts │ │ │ ├── diff-image-to-screenshot.ts │ │ │ ├── fix-screenshot-file-name.ts │ │ │ ├── get-action-set.ts │ │ │ ├── get-actions-schema.ts │ │ │ ├── get-favourite-actions.ts │ │ │ ├── get-schema-service.ts │ │ │ ├── get-story-screenshots-data.ts │ │ │ ├── get-theme-data.ts │ │ │ ├── index.ts │ │ │ ├── make-screenshot.ts │ │ │ ├── save-action-set.ts │ │ │ ├── save-screenshot.ts │ │ │ ├── test-file-screenshots.ts │ │ │ ├── test-screenshot-service.ts │ │ │ ├── test-screenshots-service.ts │ │ │ ├── test-story-screenshots.ts │ │ │ ├── update-screenshot-service.ts │ │ │ └── utils │ │ │ │ ├── __mocks__ │ │ │ │ ├── get-options-key.ts │ │ │ │ └── set-story-screenshot-options.ts │ │ │ │ ├── __tests__ │ │ │ │ ├── delete-empty-story.test.ts │ │ │ │ ├── delete-story-options.test.ts │ │ │ │ ├── find-screenshot-with-same-setting.test.tsx │ │ │ │ ├── get-options-key.test.ts │ │ │ │ ├── get-screenshot-data.test.ts │ │ │ │ ├── get-story-data.test.ts │ │ │ │ ├── get-story-playwright-data-by-file-name.test.ts │ │ │ │ ├── release-modifier-Key.test.ts │ │ │ │ ├── set-story-options.test.ts │ │ │ │ └── should-take-screenshot.test.ts │ │ │ │ ├── delete-empty-story.ts │ │ │ │ ├── delete-story-options.ts │ │ │ │ ├── find-screenshot-with-same-setting.tsx │ │ │ │ ├── get-options-key.ts │ │ │ │ ├── get-screenshot-data.ts │ │ │ │ ├── get-story-data.ts │ │ │ │ ├── get-story-playwright-data-by-file-name.ts │ │ │ │ ├── index.ts │ │ │ │ ├── is-interactive-action.ts │ │ │ │ ├── release-modifier-Key.ts │ │ │ │ ├── set-story-options.ts │ │ │ │ ├── set-story-screenshot-options.ts │ │ │ │ └── should-take-screenshot.ts │ │ └── utils │ │ │ ├── __mocks__ │ │ │ ├── execute-action.ts │ │ │ ├── install-mouse-helper.ts │ │ │ ├── load-story-data.ts │ │ │ └── save-story-file.ts │ │ │ ├── __tests__ │ │ │ ├── construct-screenshot-file-name.test.ts │ │ │ ├── execute-action.test.ts │ │ │ ├── get-screenshot-paths.test.ts │ │ │ ├── get-story-id-form-screenshot-file-name.test.ts │ │ │ ├── get-story-playwright-data.test.ts │ │ │ ├── get-story-playwright-file-info.test.ts │ │ │ ├── load-story-data.test.ts │ │ │ └── save-story-file.test.ts │ │ │ ├── construct-screenshot-file-name.ts │ │ │ ├── execute-action.ts │ │ │ ├── get-screenshot-paths.ts │ │ │ ├── get-story-id-form-screenshot-file-name.ts │ │ │ ├── get-story-playwright-data.ts │ │ │ ├── get-story-playwright-file-info.ts │ │ │ ├── get-version.ts │ │ │ ├── index.ts │ │ │ ├── install-mouse-helper.ts │ │ │ ├── load-story-data.ts │ │ │ └── save-story-file.ts │ └── typings │ │ ├── delete-action-set.ts │ │ ├── delete-favourite-action.ts │ │ ├── favourite-actions.ts │ │ ├── image-diff.ts │ │ ├── index.ts │ │ ├── save-action-set.ts │ │ ├── schema-types.ts │ │ ├── screenshot-file-name.ts │ │ ├── screenshot-request-response.ts │ │ └── test-screenshots.ts ├── components │ ├── Clipper │ │ ├── Clipper.tsx │ │ └── ClipperButton.tsx │ ├── ResizeBrowserToPreview │ │ └── ResizeBrowserToPreview.tsx │ ├── action-set-panel │ │ ├── ActionMenu.tsx │ │ ├── ActionMenuItem.tsx │ │ ├── ActionSetEditor.tsx │ │ ├── ActionSetEditorIcons.tsx │ │ ├── ActionSetList.tsx │ │ ├── ActionSetListItem.tsx │ │ ├── ActionSetMain.tsx │ │ ├── ActionSetToolbar.tsx │ │ ├── AddFavouriteAction.tsx │ │ ├── FavouriteActions.tsx │ │ ├── __tests__ │ │ │ ├── ActionMenu.test.tsx │ │ │ ├── ActionMenuItem.test.tsx │ │ │ ├── ActionSetEditor.test.tsx │ │ │ ├── ActionSetEditorIcons.test.tsx │ │ │ ├── ActionSetList.test.tsx │ │ │ ├── ActionSetListItem.test.tsx │ │ │ ├── ActionSetMain.test.tsx │ │ │ ├── ActionSetToolbar.test.tsx │ │ │ ├── AddFavouriteAction.test.tsx │ │ │ └── FavouriteActions.test.tsx │ │ ├── index.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── get-menu.test.ts.snap │ │ │ ├── filter-favourite-actions.test.ts │ │ │ └── get-menu.test.ts │ │ │ ├── filter-favourite-actions.ts │ │ │ ├── get-menu.ts │ │ │ └── index.ts │ ├── actions │ │ ├── ActionList.tsx │ │ ├── ActionOptions.tsx │ │ ├── ActionSchemaRenderer.tsx │ │ ├── __tests__ │ │ │ ├── ActionList.test.tsx │ │ │ ├── ActionOptions.test.tsx │ │ │ ├── ActionSchemaRenderer.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── ActionList.test.tsx.snap │ │ └── utils │ │ │ ├── get-action-option-value.ts │ │ │ └── index.ts │ ├── common │ │ ├── ActionDialog.tsx │ │ ├── ActionPanel.tsx │ │ ├── ActionPopover.tsx │ │ ├── BrowserIcon.tsx │ │ ├── BrowserIconButton.tsx │ │ ├── CheckBox.tsx │ │ ├── CommonProvider.tsx │ │ ├── ConfirmationPopover.tsx │ │ ├── DeleteConfirmationButton.tsx │ │ ├── DeviceList.tsx │ │ ├── DeviceListItem.tsx │ │ ├── Dialog.tsx │ │ ├── DragHandle.tsx │ │ ├── ErrorPanel.tsx │ │ ├── FixScreenshotFileDialog.tsx │ │ ├── IframeOverlay.tsx │ │ ├── ImageDiffMessage.tsx │ │ ├── ImageDiffPreview.tsx │ │ ├── ImageDiffPreviewDialog.tsx │ │ ├── ImagePreview.tsx │ │ ├── InputDialog.tsx │ │ ├── ListItemWrapper.tsx │ │ ├── ListWrapper.tsx │ │ ├── Loader.tsx │ │ ├── SchemaFormLoader.tsx │ │ ├── Snackbar.tsx │ │ ├── SnackbarContent.tsx │ │ ├── ThemeProvider.tsx │ │ ├── Toolbar.tsx │ │ ├── __tests__ │ │ │ ├── ActionDialog.test.tsx │ │ │ ├── ActionPanel.test.tsx │ │ │ ├── ActionPopover.test.tsx │ │ │ ├── BrowserIcon.test.tsx │ │ │ ├── BrowserIconButton.test.tsx │ │ │ ├── CheckBox.test.tsx │ │ │ ├── CommonProvider.test.tsx │ │ │ ├── ConfirmationPopover.test.tsx │ │ │ ├── DeleteConfirmationButton.test.tsx │ │ │ ├── DeviceList.test.tsx │ │ │ ├── DeviceListItem.test.tsx │ │ │ ├── Dialog.test.tsx │ │ │ ├── DragHandle.test.tsx │ │ │ ├── ErrorPanel.test.tsx │ │ │ ├── FixScreenshotFileDialog.test.tsx │ │ │ ├── ImageDiffMessage.test.tsx │ │ │ ├── ImageDiffPreview.test.tsx │ │ │ ├── ImageDiffPreviewDialog.test.tsx │ │ │ ├── ImagePreview.test.tsx │ │ │ ├── InputDialog.test.tsx │ │ │ ├── ListItemWrapper.test.tsx │ │ │ ├── ListWrapper.test.tsx │ │ │ ├── Loader.test.tsx │ │ │ ├── SchemaFormLoader.test.tsx │ │ │ ├── Snackbar.test.tsx │ │ │ ├── SnackbarContent.test.tsx │ │ │ ├── ThemeProvider.test.tsx │ │ │ ├── Toolbar.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── ThemeProvider.test.tsx.snap │ │ └── index.ts │ ├── index.ts │ ├── panel │ │ ├── ActionPanel.tsx │ │ ├── ScreenshotPanel.tsx │ │ ├── __tests__ │ │ │ ├── ActionPanel.test.tsx │ │ │ └── ScreenshotPanel.test.tsx │ │ └── index.ts │ ├── preview │ │ ├── EditScreenshotAlert.tsx │ │ ├── Preview.tsx │ │ ├── Selector.tsx │ │ ├── SelectorOverlay.tsx │ │ ├── __tests__ │ │ │ ├── EditScreenshotAlert.test.tsx │ │ │ ├── Preview.test.tsx │ │ │ ├── Selector.test.tsx │ │ │ └── SelectorOverlay.test.tsx │ │ ├── index.ts │ │ └── utils │ │ │ ├── __mocks__ │ │ │ └── is-horizontal.ts │ │ │ ├── __tests__ │ │ │ └── is-horizontal.test.ts │ │ │ ├── index.ts │ │ │ └── is-horizontal.ts │ ├── schema │ │ ├── Control.tsx │ │ ├── FormControl.tsx │ │ ├── SchemaProp.tsx │ │ ├── SchemaRenderer.tsx │ │ ├── SelectorControl.tsx │ │ ├── __tests__ │ │ │ ├── Control.test.tsx │ │ │ ├── FormControl.test.tsx │ │ │ ├── SchemaProp.test.tsx │ │ │ ├── SchemaRenderer.test.tsx │ │ │ └── SelectorControl.test.tsx │ │ └── index.ts │ ├── screenshot-panel │ │ ├── ScreenshotDelete.tsx │ │ ├── ScreenshotInfo.tsx │ │ ├── ScreenshotList.tsx │ │ ├── ScreenshotListItem.tsx │ │ ├── ScreenshotListItemMenu.tsx │ │ ├── ScreenshotListItemWrapper.tsx │ │ ├── ScreenshotListPreviewDialog.tsx │ │ ├── ScreenshotListSortable.tsx │ │ ├── ScreenshotListToolbar.tsx │ │ ├── ScreenshotMain.tsx │ │ ├── ScreenshotPanel.tsx │ │ ├── ScreenshotPreviewDialog.tsx │ │ ├── ScreenshotUpdate.tsx │ │ ├── StoryScreenshotPreview.tsx │ │ ├── __tests__ │ │ │ ├── ScreenshotDelete.test.tsx │ │ │ ├── ScreenshotInfo.test.tsx │ │ │ ├── ScreenshotList.test.tsx │ │ │ ├── ScreenshotListItem.test.tsx │ │ │ ├── ScreenshotListItemMenu.test.tsx │ │ │ ├── ScreenshotListItemWrapper.test.tsx │ │ │ ├── ScreenshotListPreviewDialog.test.tsx │ │ │ ├── ScreenshotListSortable.test.tsx │ │ │ ├── ScreenshotListToolbar.test.tsx │ │ │ ├── ScreenshotMain.test.tsx │ │ │ ├── ScreenshotPanel.test.tsx │ │ │ ├── ScreenshotPreviewDialog.test.tsx │ │ │ ├── ScreenshotUpdate.test.tsx │ │ │ └── StoryScreenshotPreview.test.tsx │ │ └── index.ts │ ├── screenshot-preview │ │ ├── BrowserOptions.tsx │ │ ├── OptionPopover.tsx │ │ ├── PreviewDialog.tsx │ │ ├── ResetSettings.tsx │ │ ├── ScreenShotViewToolbar.tsx │ │ ├── ScreenshotListView.tsx │ │ ├── ScreenshotOptions.tsx │ │ ├── ScreenshotView.tsx │ │ ├── Toolbar.tsx │ │ ├── __tests__ │ │ │ ├── BrowserOptions.test.tsx │ │ │ ├── OptionPopover.test.tsx │ │ │ ├── PreviewDialog.test.tsx │ │ │ ├── ScreenShotViewToolbar.test.tsx │ │ │ ├── ScreenshotListView.test.tsx │ │ │ ├── ScreenshotOptions.test.tsx │ │ │ ├── ScreenshotView.test.tsx │ │ │ └── Toolbar.test.tsx │ │ ├── index.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ └── get-border-color.test.ts │ │ │ ├── get-border-color.ts │ │ │ └── index.ts │ └── tool-bar │ │ ├── ImageDiff.tsx │ │ ├── ImageDiffMenuItem.tsx │ │ ├── PreviewPlacementMenu.tsx │ │ ├── ScreenshotUpdateIcon.tsx │ │ ├── Tool.tsx │ │ ├── __tests__ │ │ ├── ImageDiff.test.tsx │ │ ├── ImageDiffMenuItem.test.tsx │ │ ├── PreviewPlacementMenu.test.tsx │ │ ├── ScreenshotUpdateIcon.test.tsx │ │ └── Tool.test.tsx │ │ └── index.ts ├── constants │ ├── add-on.ts │ ├── index.ts │ ├── messages.ts │ └── routes.ts ├── data │ └── deviceDescriptorsSource.json ├── get-screenshots.ts ├── hooks │ ├── __mocks__ │ │ ├── use-action-editor.ts │ │ ├── use-action-schema-loader.ts │ │ ├── use-addon-state.ts │ │ ├── use-anchor-el.ts │ │ ├── use-async-api-call.tsx │ │ ├── use-browser-options.ts │ │ ├── use-current-actions.ts │ │ ├── use-current-story-action-sets.ts │ │ ├── use-current-story-data.ts │ │ ├── use-custom-theme.ts │ │ ├── use-delete-screenshot.ts │ │ ├── use-drag-start.ts │ │ ├── use-edit-screenshot.ts │ │ ├── use-editor-action.ts │ │ ├── use-global-action-dispatch.ts │ │ ├── use-global-dispatch.ts │ │ ├── use-global-imageDiff-results.ts │ │ ├── use-global-screenshot-dispatch.ts │ │ ├── use-global-state1.ts │ │ ├── use-imagediff-screenshots.ts │ │ ├── use-load-screenshot-settings.ts │ │ ├── use-save-screenshot.ts │ │ ├── use-screenshot-imageDiff-results.ts │ │ ├── use-screenshot-imageDiff.ts │ │ ├── use-screenshot-index-change.ts │ │ ├── use-screenshot-options.ts │ │ ├── use-screenshot-update-state.ts │ │ ├── use-screenshot-update.ts │ │ ├── use-screenshot.ts │ │ ├── use-selector-manager.ts │ │ ├── use-snackbar.tsx │ │ └── use-story-action-sets-loader.ts │ ├── __tests__ │ │ ├── use-action-editor.test.ts │ │ ├── use-action-schema-loader.test.ts │ │ ├── use-active-browser.test.ts │ │ ├── use-addon-state.test.ts │ │ ├── use-async-api-call.test.tsx │ │ ├── use-browser-options.test.ts │ │ ├── use-control.test.ts │ │ ├── use-copy-action-set.test.ts │ │ ├── use-current-actions.test.ts │ │ ├── use-current-story-action-sets.test.ts │ │ ├── use-current-story-data.test.ts │ │ ├── use-custom-theme.test.ts │ │ ├── use-delete-screenshot.test.ts │ │ ├── use-delete-story-screenshots.test.ts │ │ ├── use-drag-start.test.ts │ │ ├── use-edit-screenshot.test.ts │ │ ├── use-editor-action.test.ts │ │ ├── use-fix-screenshot-file-name.test.ts │ │ ├── use-global-dispatch.test.ts │ │ ├── use-global-imageDiff-results.test.ts │ │ ├── use-global-screenshot-dispatch.test.ts │ │ ├── use-global-state.test.ts │ │ ├── use-imagediff-screenshots.test.ts │ │ ├── use-key-press.test.ts │ │ ├── use-knobs.test.ts │ │ ├── use-load-screenshot-settings.test.ts │ │ ├── use-reset-setting.test.ts │ │ ├── use-save-screenshot.test.ts │ │ ├── use-screenshot-imageDiff-results.test.ts │ │ ├── use-screenshot-imageDiff.test.ts │ │ ├── use-screenshot-index-change.test.ts │ │ ├── use-screenshot-update-state.test.ts │ │ ├── use-screenshot-update.test.ts │ │ ├── use-screenshot.test.ts │ │ ├── use-selector-manager.test.ts │ │ ├── use-snackbar.test.tsx │ │ ├── use-story-action-sets-loader.test.ts │ │ ├── use-story-screenshot-loader.test.ts │ │ └── use-story-url.test.ts │ ├── index.ts │ ├── use-action-editor.ts │ ├── use-action-schema-loader.ts │ ├── use-active-browser.ts │ ├── use-addon-state.ts │ ├── use-anchor-el.ts │ ├── use-async-api-call.tsx │ ├── use-browser-options.ts │ ├── use-control.ts │ ├── use-copy-action-set.ts │ ├── use-current-actions.ts │ ├── use-current-story-action-sets.ts │ ├── use-current-story-data.ts │ ├── use-custom-theme.ts │ ├── use-delete-screenshot.ts │ ├── use-delete-story-screenshots.ts │ ├── use-drag-start.ts │ ├── use-edit-screenshot.ts │ ├── use-editor-action.ts │ ├── use-fix-screenshot-file-name.ts │ ├── use-global-action-dispatch.ts │ ├── use-global-dispatch.ts │ ├── use-global-imageDiff-results.ts │ ├── use-global-screenshot-dispatch.ts │ ├── use-global-state.ts │ ├── use-imagediff-screenshots.ts │ ├── use-key-press-fn.ts │ ├── use-key-press.ts │ ├── use-knobs.ts │ ├── use-load-screenshot-settings.ts │ ├── use-preview-iframe.ts │ ├── use-reset-setting.ts │ ├── use-save-screenshot.ts │ ├── use-screenshot-imageDiff-results.ts │ ├── use-screenshot-imageDiff.ts │ ├── use-screenshot-index-change.ts │ ├── use-screenshot-options.ts │ ├── use-screenshot-update-state.ts │ ├── use-screenshot-update.ts │ ├── use-screenshot.ts │ ├── use-selector-manager.ts │ ├── use-snackbar.tsx │ ├── use-story-action-sets-loader.ts │ ├── use-story-screenshot-loader.ts │ └── use-story-url.ts ├── icons │ ├── Browser.tsx │ ├── Chrome.tsx │ ├── Firefox.tsx │ ├── LayoutBottom.tsx │ ├── LayoutBottomRight.tsx │ ├── LayoutRight.tsx │ ├── TestIcon.tsx │ ├── Webkit.tsx │ ├── __tests__ │ │ ├── Browser.test.tsx │ │ ├── Chrome.test.tsx │ │ ├── Firefox.test.tsx │ │ ├── LayoutBottom.test.tsx │ │ ├── LayoutBottomRight.test.tsx │ │ ├── LayoutRight.test.tsx │ │ ├── TestIcon.test.tsx │ │ └── Webkit.test.tsx │ └── index.ts ├── index.ts ├── page-extra │ ├── __tests__ │ │ ├── clear-input.test.ts │ │ ├── drag-drop-selector.test.ts │ │ ├── mouse-down-on-selector.test.ts │ │ ├── mouse-from-to.test.ts │ │ ├── mouse-move-to-selector.test.ts │ │ ├── scroll-selector.test.ts │ │ ├── selector-mouse-wheel.test.ts │ │ ├── set-selector-size.test.ts │ │ ├── touch-cancel.test.ts │ │ ├── touch-end.test.ts │ │ ├── touch-from-to.test.ts │ │ ├── touch-move.test.ts │ │ └── touch-start.test.ts │ ├── clear-input.ts │ ├── drag-drop-selector.ts │ ├── extend-page.ts │ ├── index.ts │ ├── mouse-down-on-selector.ts │ ├── mouse-from-to.ts │ ├── mouse-move-to-selector.ts │ ├── scroll-selector.ts │ ├── selector-mouse-wheel.ts │ ├── set-selector-size.ts │ ├── touch-cancel.ts │ ├── touch-end.ts │ ├── touch-from-to.ts │ ├── touch-move.ts │ ├── touch-start.ts │ ├── typings │ │ ├── extra.ts │ │ └── index.ts │ └── utils │ │ ├── __tests__ │ │ ├── dispatch-touch-event.test.ts │ │ └── get-boundingBox.test.ts │ │ ├── dispatch-touch-event.ts │ │ ├── get-boundingBox.ts │ │ ├── get-point-by-direction.ts │ │ └── index.ts ├── preset.ts ├── register.tsx ├── run-image-diff.ts ├── store │ ├── actions │ │ ├── ActionContext.tsx │ │ ├── __mocks__ │ │ │ └── ActionContext.tsx │ │ ├── __tests__ │ │ │ ├── ActionContext.test.tsx │ │ │ └── reducer.test.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ └── is-same-actions.test.ts │ │ │ ├── index.ts │ │ │ └── is-same-actions.ts │ ├── index.ts │ └── screenshot │ │ ├── __mocks__ │ │ └── context.tsx │ │ ├── __tests__ │ │ ├── context.test.tsx │ │ └── reducer.test.ts │ │ ├── context.tsx │ │ ├── index.ts │ │ └── reducer.ts ├── to-match-screenshots.ts ├── typings │ ├── action-schema.ts │ ├── addon-state.ts │ ├── compare-screenshot.ts │ ├── config.ts │ ├── control.ts │ ├── index.ts │ ├── knobs.ts │ ├── request.ts │ ├── screenshot.ts │ ├── selector.ts │ ├── story-action.ts │ ├── story-data.ts │ ├── story-info.ts │ └── storybook.ts └── utils │ ├── __mocks__ │ ├── get-playwright-config-files.ts │ ├── get-preview-iframe.ts │ └── valid-action.ts │ ├── __tests__ │ ├── capitalize.test.ts │ ├── combine-reducer.test.ts │ ├── construct-story-url.test.ts │ ├── get-action-args.test.ts │ ├── get-device-info.test.ts │ ├── get-raw-stories.test.ts │ ├── get-schema.test.ts │ ├── get-story-function.test.ts │ ├── is-story-json-file.test.ts │ ├── knobs-to-querystring.test.ts │ └── valid-action.test.ts │ ├── capitalize.ts │ ├── combine-reducer.ts │ ├── construct-story-url.ts │ ├── find-selector.ts │ ├── get-action-args.ts │ ├── get-device-info.ts │ ├── get-iframe-document.ts │ ├── get-iframe-scroll-position.ts │ ├── get-image-diff-messages.ts │ ├── get-playwright-config-files.ts │ ├── get-preview-iframe.ts │ ├── get-raw-stories.ts │ ├── get-schema.ts │ ├── get-story-function.ts │ ├── index.ts │ ├── is-story-json-file.ts │ ├── is-valid-selector.ts │ ├── knobs-to-querystring.ts │ └── valid-action.ts ├── stories ├── Boxes.stories.playwright.json ├── Boxes.stories.tsx ├── InputWheel.stories.playwright.json ├── InputWheel.stories.tsx ├── InputWheel.tsx ├── Mouse.stories.playwright.json ├── Mouse.stories.tsx ├── Touch.stories.playwright.json ├── Touch.stories.tsx ├── __screenshots__ │ ├── boxes-with-default-should-allow-last-screenshot-if-last-action-is-not-take-element-screenshot-chromium-snap.png │ ├── boxes-with-default-should-take-screen-shot-by-element-id-chromium-snap.png │ ├── inputwheel-with-input-wheel-should-wait-for-input-value-to-change-to-1-and-change-input-value-afterwards-chromium-snap.png │ ├── mouse-with-draggable-should-drag-chromium-snap.png │ ├── mouse-with-tippy-click-chromium-snap.png │ ├── mouse-with-tippy-should-hover-chromium-snap.png │ └── touch-with-default-should-handle-touch-events-chromium-snap.png └── __tests__ │ └── first.stories.test.tsx ├── tsconfig.json ├── tsconfig.release.json ├── typings └── global.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.md] 10 | insert_final_newline = false 11 | trim_trailing_whitespace = false 12 | 13 | [*.{js,jsx,json,ts,tsx,yml}] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | # don't ever lint node_modules 3 | node_modules 4 | # don't lint build output (make sure it's set to your correct build folder name) 5 | dist 6 | # don't lint nyc coverage output 7 | coverage 8 | 9 | /**/*.js 10 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run lint --if-present 22 | - run: npm run build --if-present 23 | - run: npm test 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | report* 6 | 7 | # Dependencies 8 | node_modules/ 9 | 10 | # Coverage 11 | coverage 12 | 13 | # Transpiled files 14 | dist/ 15 | 16 | # VS Code 17 | .vscode/* 18 | !.vscode/settings.json 19 | !.vscode/tasks.json 20 | !.vscode/launch.json 21 | !.vscode/extensions.json 22 | *.code-workspace 23 | 24 | # JetBrains IDEs 25 | .idea/ 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # Misc 34 | .DS_Store 35 | 36 | # Local History for Visual Studio Code 37 | .history/ 38 | # __screenshots__/**/* 39 | # *.playwright.json 40 | # *snap.png 41 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "overrides": [ 5 | { 6 | "files": "*.ts", 7 | "options": { 8 | "parser": "typescript" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '../register'; 2 | import '@storybook/addon-knobs/dist/register'; 3 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | stories: ['../stories/*.stories.[tj]sx'], 4 | addons: [ 5 | 'storybook-dark-mode/register', 6 | path.resolve(__dirname, '../preset.js'), 7 | ], 8 | features: { 9 | storyStoreV7: true, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.storybook/middleware.js: -------------------------------------------------------------------------------- 1 | const middleware = require('../middleware'); 2 | const { setupPlaywright } = require('./setup-playwright'); 3 | 4 | (async () => { 5 | await setupPlaywright(); 6 | })(); 7 | 8 | module.exports = middleware; 9 | -------------------------------------------------------------------------------- /.storybook/playwright-favourite-actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "actionSets": [] 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { themes } from '@storybook/theming'; 2 | export const parameters = { 3 | darkMode: { 4 | // Override the default dark theme 5 | dark: { ...themes.dark }, 6 | // Override the default light theme 7 | light: { ...themes.normal }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config }) => { 2 | return config; 3 | }; 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "autorun", 4 | "dblclick", 5 | "deepmerge", 6 | "draggables", 7 | "droppable", 8 | "mobx", 9 | "normalazed", 10 | "pkginfo", 11 | "Resizer", 12 | "scrl", 13 | "scrollbar", 14 | "Selecto", 15 | "tippyjs", 16 | "Toolshown" 17 | ], 18 | "liveServer.settings.fileList": [ 19 | { 20 | "label": "jest", 21 | "relativePath": "coverage/lcov-report/index.html" 22 | } 23 | ], 24 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "type": "shell", 9 | "group": "build", 10 | "command": "npm run watch", 11 | "problemMatcher": [], 12 | "runOptions": { 13 | "runOn": "folderOpen", 14 | } 15 | }, 16 | { 17 | "label": "storybook start", 18 | "type": "shell", 19 | "command": "npm run start:storybook", 20 | "presentation": { 21 | "reveal": "always", 22 | "revealProblems": "onProblem", 23 | "close": true 24 | }, 25 | "problemMatcher": [], 26 | "runOptions": { 27 | "runOn": "folderOpen", 28 | } 29 | }, 30 | ] 31 | } -------------------------------------------------------------------------------- /__manual_mocks__/hooks/use-active-browser.ts: -------------------------------------------------------------------------------- 1 | export const useActiveBrowserMock = jest.fn(); 2 | export const isDisabledMock = jest.fn(); 3 | export const toggleBrowserMock = jest.fn(); 4 | 5 | jest.mock('../../src/hooks/use-active-browser', () => ({ 6 | useActiveBrowsers: useActiveBrowserMock, 7 | })); 8 | 9 | useActiveBrowserMock.mockImplementation(() => ({ 10 | activeBrowsers: ['chromium', 'firefox', 'webkit'], 11 | isDisabled: isDisabledMock, 12 | toggleBrowser: toggleBrowserMock, 13 | })); 14 | -------------------------------------------------------------------------------- /__manual_mocks__/hooks/use-current-story-action-sets.ts: -------------------------------------------------------------------------------- 1 | import { ActionSet } from '../../src/typings'; 2 | 3 | export const useCurrentStoryActionSetsMock = jest.fn(); 4 | 5 | jest.mock('../../src/hooks/use-current-story-action-sets', () => ({ 6 | useCurrentStoryActionSets: useCurrentStoryActionSetsMock, 7 | })); 8 | 9 | useCurrentStoryActionSetsMock.mockImplementation(() => ({ 10 | currentActionSets: ['action-set-id'], 11 | storyActionSets: [ 12 | { 13 | actions: [ 14 | { 15 | id: 'action-id', 16 | name: 'click', 17 | }, 18 | ], 19 | id: 'action-set-id', 20 | }, 21 | ] as ActionSet[], 22 | })); 23 | -------------------------------------------------------------------------------- /__manual_mocks__/hooks/use-global-action-dispatch.ts: -------------------------------------------------------------------------------- 1 | export const dispatchMock = jest.fn(); 2 | 3 | jest.mock('../../src/hooks/use-global-action-dispatch', () => ({ 4 | useGlobalActionDispatch: () => ({ 5 | dispatch: dispatchMock, 6 | }), 7 | })); 8 | -------------------------------------------------------------------------------- /__manual_mocks__/hooks/use-global-screenshot-dispatch.ts: -------------------------------------------------------------------------------- 1 | export const globalDispatchMock = jest.fn(); 2 | 3 | jest.mock('../../src/hooks/use-global-screenshot-dispatch', () => ({ 4 | useGlobalScreenshotDispatch: () => ({ 5 | dispatch: globalDispatchMock, 6 | }), 7 | })); 8 | -------------------------------------------------------------------------------- /__manual_mocks__/hooks/use-screenshot.ts: -------------------------------------------------------------------------------- 1 | export const useScreenshotMock = jest.fn(); 2 | export const getSnapshotMock = jest.fn(); 3 | 4 | jest.mock('../../src/hooks/use-screenshot', () => ({ 5 | useScreenshot: useScreenshotMock, 6 | })); 7 | 8 | useScreenshotMock.mockImplementation(() => { 9 | return { 10 | getSnapshot: getSnapshotMock, 11 | loading: false, 12 | screenshot: { 13 | base64: 'base64-image', 14 | error: undefined, 15 | }, 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /__manual_mocks__/hooks/use-story-action-sets-loader.ts: -------------------------------------------------------------------------------- 1 | export const useStoryActionSetsLoaderMock = jest.fn(); 2 | 3 | jest.mock('../../src/hooks/use-story-action-sets-loader', () => ({ 4 | useStoryActionSetsLoader: useStoryActionSetsLoaderMock, 5 | })); 6 | 7 | export const useStoryActionSetsLoaderRetryMock = jest.fn(); 8 | 9 | useStoryActionSetsLoaderMock.mockImplementation(() => ({ 10 | error: undefined, 11 | loading: false, 12 | retry: useStoryActionSetsLoaderRetryMock, 13 | })); 14 | -------------------------------------------------------------------------------- /__manual_mocks__/jest-image-snapshot.ts: -------------------------------------------------------------------------------- 1 | import { ImageDiffResult } from '../src/api/typings'; 2 | 3 | export const runDiffImageToSnapshotMock = jest.fn() as jest.Mock< 4 | ImageDiffResult 5 | >; 6 | 7 | jest.mock('jest-image-snapshot/src/diff-snapshot', () => ({ 8 | runDiffImageToSnapshot: runDiffImageToSnapshotMock, 9 | })); 10 | 11 | runDiffImageToSnapshotMock.mockImplementation(() => { 12 | return { 13 | added: true, 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /__manual_mocks__/playwright/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pageProps'; 2 | -------------------------------------------------------------------------------- /__manual_mocks__/react-useEffect.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | let cleanupFns = []; 4 | 5 | beforeEach(() => { 6 | cleanupFns = []; 7 | }); 8 | 9 | jest.spyOn(React, 'useEffect').mockImplementation((func, deps) => { 10 | const ref = React.useRef(); 11 | if ( 12 | !ref.current || 13 | (ref.current !== undefined && 14 | JSON.stringify(ref.current) !== JSON.stringify(deps)) 15 | ) { 16 | ref.current = deps; 17 | const cleanupFn = func(); 18 | if (cleanupFn) { 19 | cleanupFns.push(cleanupFn); 20 | } 21 | } 22 | }); 23 | 24 | export const useEffectCleanup = () => { 25 | cleanupFns.forEach((f) => { 26 | f(); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /__manual_mocks__/store/action/context.ts: -------------------------------------------------------------------------------- 1 | import { ReducerState } from '../../../src/store/actions/reducer'; 2 | import { useActionDispatchContext } from '../../../src/store/actions/ActionContext'; 3 | import { mocked } from 'ts-jest/utils'; 4 | 5 | jest.mock('../../../src/store/actions/ActionContext'); 6 | 7 | export const dispatchMock = jest.fn(); 8 | 9 | const useActionDispatchContextMock = mocked(useActionDispatchContext); 10 | 11 | useActionDispatchContextMock.mockImplementation(() => { 12 | return (...arg) => { 13 | return dispatchMock(arg); 14 | }; 15 | }); 16 | 17 | export { ReducerState }; 18 | -------------------------------------------------------------------------------- /__manual_mocks__/store/screenshot/context.ts: -------------------------------------------------------------------------------- 1 | import { ReducerState } from '../../../src/store/screenshot/reducer'; 2 | 3 | jest.unmock('../../../src/store/screenshot/context'); 4 | 5 | export const dispatchMock = jest.fn(); 6 | 7 | const mockData: Partial = { 8 | screenshots: [], 9 | }; 10 | 11 | export const useScreenShotContext = jest.fn() as jest.Mock< 12 | Partial 13 | >; 14 | useScreenShotContext.mockImplementation(() => mockData); 15 | 16 | jest.mock('../../../src/store/screenshot/context', () => ({ 17 | useScreenshotContext: useScreenShotContext, 18 | useScreenshotDispatch: () => { 19 | return (...arg) => { 20 | return dispatchMock(arg); 21 | }; 22 | }, 23 | })); 24 | 25 | export { ReducerState }; 26 | 27 | export default ReducerState; 28 | -------------------------------------------------------------------------------- /__manual_mocks__/ts-to-json.ts: -------------------------------------------------------------------------------- 1 | export const createProgramMock = jest.fn(); 2 | export const createParserMock = jest.fn(); 3 | export const createFormatterMock = jest.fn(); 4 | export const SchemaGeneratorMock = jest.fn().mockImplementation(() => { 5 | return { 6 | createSchema: () => ({ 7 | definitions: { ['MyType']: { properties: { props: true } } }, 8 | }), 9 | }; 10 | }); 11 | jest.mock('ts-to-json', () => ({ 12 | SchemaGenerator: SchemaGeneratorMock, 13 | createFormatter: createFormatterMock, 14 | createParser: createParserMock, 15 | createProgram: createProgramMock, 16 | })); 17 | -------------------------------------------------------------------------------- /__manual_mocks__/utils/load-story-data.ts: -------------------------------------------------------------------------------- 1 | import { storyFileInfo } from '../../__test_data__/story-file-info'; 2 | 3 | export const loadStoryDataMock = jest.fn(); 4 | 5 | jest.unmock('../../src/api/server/utils/load-story-data'); 6 | 7 | jest.mock('../../src/api/server/utils/load-story-data', () => ({ 8 | loadStoryData: loadStoryDataMock, 9 | })); 10 | 11 | loadStoryDataMock.mockImplementation(() => { 12 | const data = storyFileInfo(); 13 | return new Promise((resolve) => { 14 | resolve(data); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /__mocks__/@storybook/addons.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { EventEmitter } from 'events'; 3 | import { types as addonsTypes } from '@storybook/addons'; 4 | 5 | const addonsMock = jest.createMockFromModule('@storybook/addons') as any; 6 | export const types = addonsTypes; 7 | 8 | const ee = new EventEmitter(); 9 | 10 | addonsMock.getChannel = () => ({ 11 | emit: ee.emit.bind(ee), 12 | off: ee.off.bind(ee), 13 | on: ee.on.bind(ee), 14 | }); 15 | 16 | addonsMock.__setEvent = (eve: string, val: unknown) => { 17 | ee.emit(eve, val); 18 | }; 19 | 20 | addonsMock.register = (_id: string, fn: () => void) => fn(); 21 | 22 | addonsMock.add = jest.fn(); 23 | 24 | export default addonsMock; 25 | -------------------------------------------------------------------------------- /__mocks__/fast-glob.ts: -------------------------------------------------------------------------------- 1 | const glob = () => { 2 | return new Promise((resolve) => { 3 | resolve(['story.ts']); 4 | }); 5 | }; 6 | 7 | export default glob; 8 | -------------------------------------------------------------------------------- /__mocks__/fix-screenshot-file-name.ts: -------------------------------------------------------------------------------- 1 | export const fixScreenshotFileName = jest.fn(); 2 | -------------------------------------------------------------------------------- /__mocks__/get-story-playwright-data-by-file-name.ts: -------------------------------------------------------------------------------- 1 | export const getStoryPlaywrightDataByFileName = jest.fn(); 2 | -------------------------------------------------------------------------------- /__mocks__/get-story-playwright-file-info.ts: -------------------------------------------------------------------------------- 1 | export const getStoryPlaywrightFileInfo = jest.fn(); 2 | -------------------------------------------------------------------------------- /__mocks__/jest-image-snapshot.ts: -------------------------------------------------------------------------------- 1 | import { toMatchImageSnapshot as toMatchImageSnapshotOrg } from 'jest-image-snapshot'; 2 | export const toMatchImageSnapshot = jest.fn(); 3 | 4 | toMatchImageSnapshot.mockImplementation(toMatchImageSnapshotOrg); 5 | -------------------------------------------------------------------------------- /__mocks__/nanoid.ts: -------------------------------------------------------------------------------- 1 | let id = 0; 2 | 3 | export function nanoid() { 4 | id++; 5 | return 'id-' + id; 6 | } 7 | -------------------------------------------------------------------------------- /__mocks__/notistack.ts: -------------------------------------------------------------------------------- 1 | export const useSnackbar = jest.fn().mockImplementation(() => { 2 | return { 3 | closeSnackbar: jest.fn(), 4 | enqueueSnackbar: jest.fn(), 5 | }; 6 | }); 7 | 8 | export const SnackbarProvider = jest.fn(); 9 | -------------------------------------------------------------------------------- /__mocks__/react-use/lib/useKey.ts: -------------------------------------------------------------------------------- 1 | const useKey = jest.fn(); 2 | 3 | export default useKey; 4 | -------------------------------------------------------------------------------- /__mocks__/react-use/lib/useMouseHovered.ts: -------------------------------------------------------------------------------- 1 | const useMouseHovered = jest 2 | .fn() 3 | .mockImplementation(() => ({ elX: 10, elY: 10 })); 4 | 5 | export default useMouseHovered; 6 | -------------------------------------------------------------------------------- /__mocks__/react-use/lib/useThrottleFn.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | export const useThrottleFn = jest.fn(); 3 | useThrottleFn.mockImplementation((cb, _delay, data) => { 4 | const mounted = useRef(false); 5 | if (!mounted.current) { 6 | mounted.current = true; 7 | return; 8 | } 9 | cb(...data); 10 | }); 11 | 12 | export default useThrottleFn; 13 | -------------------------------------------------------------------------------- /__mocks__/reinspect.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | export const useReducer = jest 4 | .fn() 5 | .mockImplementation( 6 | (_reducer: unknown, initialState: unknown, initialStateFn: () => void) => { 7 | if (initialStateFn) initialStateFn(); 8 | return [initialState, jest.fn()]; 9 | }, 10 | ); 11 | 12 | export const StateInspector = () => createElement('div'); 13 | -------------------------------------------------------------------------------- /__mocks__/release-modifier-Key.ts: -------------------------------------------------------------------------------- 1 | export const releaseModifierKey = jest.fn(); 2 | -------------------------------------------------------------------------------- /__mocks__/touch.ts: -------------------------------------------------------------------------------- 1 | export const touch = jest.fn(); 2 | -------------------------------------------------------------------------------- /__mocks__/use-fix-screenshot-file-name.ts: -------------------------------------------------------------------------------- 1 | export const useFixScreenshotFileName = jest.fn(); 2 | -------------------------------------------------------------------------------- /__mocks__/use-key-press-fn.ts: -------------------------------------------------------------------------------- 1 | export const useKeyPressFn = jest.fn(); 2 | -------------------------------------------------------------------------------- /__test_data__/assets/test-image-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccpu/storybook-addon-playwright/e4d6d001af01fe145e2b2cc2fbdbec07d5c7dd39/__test_data__/assets/test-image-snap.png -------------------------------------------------------------------------------- /__test_data__/get-org-editing-actionSet.ts: -------------------------------------------------------------------------------- 1 | export const getOrgEditingActionSet = () => { 2 | return { 3 | actions: [{ id: 'action-id', name: 'action-name' }], 4 | id: 'action-set-id', 5 | title: 'action-set-desc', 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /__test_data__/get-screenshot-date.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotData } from '../src/typings'; 2 | 3 | export const getScreenshotDate = ( 4 | data?: Partial, 5 | ): ScreenshotData => { 6 | return { 7 | browserType: 'chromium', 8 | id: 'screenshot-id', 9 | title: 'title', 10 | ...data, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /__test_data__/index.ts: -------------------------------------------------------------------------------- 1 | export * from './action-schema'; 2 | -------------------------------------------------------------------------------- /__test_data__/story-data.ts: -------------------------------------------------------------------------------- 1 | import { StoryData } from '../src/typings'; 2 | 3 | export const storyData: StoryData = { 4 | id: 'story-id', 5 | isLeaf: true, 6 | kind: 'Component', 7 | name: 'With Component', 8 | parameters: { 9 | __id: 'story-id', 10 | component: { 11 | displayName: 'Component', 12 | }, 13 | fileName: './test.stories.tsx', 14 | framework: 'react', 15 | }, 16 | parent: 'component', 17 | story: 'With Component', 18 | } as unknown as StoryData; 19 | 20 | export const getStoryData = (): StoryData => storyData; 21 | -------------------------------------------------------------------------------- /assets/addon-screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccpu/storybook-addon-playwright/e4d6d001af01fe145e2b2cc2fbdbec07d5c7dd39/assets/addon-screenshot.gif -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /configs.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/api/server/configs'; 2 | -------------------------------------------------------------------------------- /configs.js: -------------------------------------------------------------------------------- 1 | const configs = require('./dist/api/server/configs'); 2 | module.exports = configs; 3 | -------------------------------------------------------------------------------- /middleware.d.ts: -------------------------------------------------------------------------------- 1 | import middleware from './dist/api/server/routes'; 2 | export default middleware; 3 | -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | const middleware = require('./dist/api/server/routes'); 2 | module.exports = middleware; 3 | -------------------------------------------------------------------------------- /preset.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/preset'); 2 | -------------------------------------------------------------------------------- /register.js: -------------------------------------------------------------------------------- 1 | require('./dist/register'); 2 | -------------------------------------------------------------------------------- /setupTests.ts: -------------------------------------------------------------------------------- 1 | import enzyme from 'enzyme'; 2 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 3 | import { toMatchImageSnapshot } from 'jest-image-snapshot'; 4 | 5 | //! uncomment this will cause problem with jest mocks 6 | // const { toMatchScreenshots } = require('./src/to-match-screenshots'); 7 | // expect.extend({ toMatchScreenshots }); 8 | 9 | expect.extend({ toMatchImageSnapshot }); 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-var-requires 12 | require('jest-fetch-mock').enableMocks(); 13 | enzyme.configure({ adapter: new Adapter() }); 14 | -------------------------------------------------------------------------------- /src/__tests__/get-screenshots.test.ts: -------------------------------------------------------------------------------- 1 | import { getScreenshots } from '../get-screenshots'; 2 | 3 | jest.mock('../api/server/utils/load-story-data.ts'); 4 | jest.mock('../api/server/services/make-screenshot.ts'); 5 | 6 | describe('getScreenshots', () => { 7 | it('should return result', async () => { 8 | const result = await getScreenshots({ 9 | playwrightJsonPath: 'localhost:3000', 10 | requestId: 'request-id', 11 | }); 12 | expect(result).toHaveLength(2); 13 | }); 14 | 15 | it('should call onScreenshotReady', async () => { 16 | const onScreenshotReadyMock = jest.fn(); 17 | await getScreenshots({ 18 | onScreenshotReady: onScreenshotReadyMock, 19 | playwrightJsonPath: 'localhost:3000', 20 | requestId: 'request-id', 21 | }); 22 | 23 | expect(onScreenshotReadyMock).toHaveBeenCalledTimes(2); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/api/client/__mocks__/delete-action-set.ts: -------------------------------------------------------------------------------- 1 | export const deleteActionSet = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/api/client/__mocks__/test-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { mocked } from 'ts-jest/utils'; 2 | import { testScreenshot as TestScreenshot } from '../test-screenshot'; 3 | export const testScreenshot = jest.fn() as jest.Mocked; 4 | mocked(testScreenshot).mockImplementation(() => { 5 | return new Promise((resolve) => { 6 | resolve({ 7 | fileName: './test.stories.tsx', 8 | newScreenshot: 'base64-image', 9 | pass: true, 10 | screenshotId: 'screenshot-id', 11 | storyId: 'story-id', 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/api/client/__mocks__/test-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { mocked } from 'ts-jest/utils'; 2 | 3 | export const testScreenshots = jest.fn(); 4 | 5 | mocked(testScreenshots).mockImplementation(() => { 6 | return new Promise((resolve) => { 7 | resolve([ 8 | { 9 | pass: true, 10 | }, 11 | ]); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/api/client/__mocks__/update-screenshot.ts: -------------------------------------------------------------------------------- 1 | export const updateScreenshot = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/api/client/__tests__/delete-action-set.test.ts: -------------------------------------------------------------------------------- 1 | import { deleteActionSet } from '../delete-action-set'; 2 | import fetch from 'jest-fetch-mock'; 3 | import { DeleteActionSetRequest } from '../../typings'; 4 | 5 | describe('deleteActionSet', () => { 6 | const data: DeleteActionSetRequest = { 7 | actionSetId: 'action-set-id', 8 | storyId: 'story-id', 9 | }; 10 | 11 | beforeEach(() => { 12 | fetch.resetMocks(); 13 | }); 14 | 15 | it('should have data in body', async () => { 16 | fetch.mockResponseOnce(JSON.stringify({})); 17 | await deleteActionSet(data); 18 | expect(fetch.mock.calls[0][1].body).toStrictEqual(JSON.stringify(data)); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/api/client/__tests__/delete-favourite-action.test.ts: -------------------------------------------------------------------------------- 1 | import { deleteFavouriteAction } from '../delete-favourite-action'; 2 | 3 | import fetch from 'jest-fetch-mock'; 4 | import { DeleteFavouriteAction } from '../../typings'; 5 | 6 | describe('deleteFavouriteAction', () => { 7 | const data: DeleteFavouriteAction = { 8 | actionSetId: 'action-set-id', 9 | }; 10 | 11 | beforeEach(() => { 12 | fetch.resetMocks(); 13 | }); 14 | 15 | it('should have data in body', async () => { 16 | fetch.mockResponseOnce(JSON.stringify({})); 17 | await deleteFavouriteAction(data); 18 | expect(fetch.mock.calls[0][1].body).toStrictEqual(JSON.stringify(data)); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/api/client/__tests__/delete-screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { deleteScreenshot } from '../delete-screenshot'; 2 | import fetch from 'jest-fetch-mock'; 3 | 4 | describe('deleteScreenshot', () => { 5 | it('should delete', async () => { 6 | const mock = fetch.mockResponseOnce(JSON.stringify({})); 7 | await deleteScreenshot({ 8 | fileName: 'story.ts', 9 | screenshotId: 'screenshot-id', 10 | storyId: 'story-id', 11 | }); 12 | expect(mock).toHaveBeenCalledWith('http://localhost/screenshot/delete', { 13 | body: 14 | '{"fileName":"story.ts","screenshotId":"screenshot-id","storyId":"story-id"}', 15 | headers: { 16 | Accept: 'application/json, text/plain, */*', 17 | 'Content-Type': 'application/json', 18 | }, 19 | method: 'post', 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/api/client/__tests__/delete-story-screenshots.test.ts: -------------------------------------------------------------------------------- 1 | import { deleteStoryScreenshots } from '../delete-story-screenshots'; 2 | import fetch from 'jest-fetch-mock'; 3 | describe('deleteStoryScreenshots', () => { 4 | it('should delete', async () => { 5 | const fetchMock = fetch.mockResponseOnce(''); 6 | 7 | await deleteStoryScreenshots({ 8 | fileName: 'file-name', 9 | storyId: 'story-id', 10 | }); 11 | 12 | expect( 13 | fetchMock, 14 | ).toHaveBeenCalledWith('http://localhost/screenshot/deleteStory', { 15 | body: '{"fileName":"file-name","storyId":"story-id"}', 16 | headers: { 17 | Accept: 'application/json, text/plain, */*', 18 | 'Content-Type': 'application/json', 19 | }, 20 | method: 'post', 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/api/client/__tests__/fix-screenshot-file-name.test.ts: -------------------------------------------------------------------------------- 1 | import { fixScreenshotFileName } from '../fix-screenshot-file-name'; 2 | import fetch from 'jest-fetch-mock'; 3 | import { getStoryData } from '../../../../__test_data__/story-data'; 4 | 5 | describe('fixScreenshotFileName', () => { 6 | beforeEach(() => { 7 | fetch.doMock(); 8 | }); 9 | it('should be defined', () => { 10 | expect(fixScreenshotFileName).toBeDefined(); 11 | }); 12 | 13 | it('should have response', async () => { 14 | fetch.mockResponseOnce(JSON.stringify(getStoryData())); 15 | const res = await fixScreenshotFileName(getStoryData()); 16 | expect(res).toStrictEqual(getStoryData()); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/client/__tests__/get-action-schema.test.ts: -------------------------------------------------------------------------------- 1 | import { getActionSchema } from '../get-action-schema'; 2 | import fetch from 'jest-fetch-mock'; 3 | import { getActionSchemaData } from '../../../../__test_data__/action-schema'; 4 | 5 | describe('getActionSchema', () => { 6 | beforeEach(() => { 7 | fetch.doMock(); 8 | }); 9 | 10 | it('should have response', async () => { 11 | fetch.mockResponseOnce(JSON.stringify(getActionSchemaData())); 12 | const res = await getActionSchema(); 13 | expect(res).toStrictEqual(getActionSchemaData()); 14 | }); 15 | 16 | it('should throw error', async () => { 17 | fetch.mockReject(new Error('foo')); 18 | await expect(getActionSchema()).rejects.toThrowError('foo'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/api/client/__tests__/get-schema-client.test.ts: -------------------------------------------------------------------------------- 1 | import { getSchemaClient } from '../get-schema-client'; 2 | import fetch from 'jest-fetch-mock'; 3 | 4 | describe('getSchemaClient', () => { 5 | it('should have request payload', async () => { 6 | const mock = fetch.mockResponseOnce(JSON.stringify({})); 7 | await getSchemaClient('MyType'); 8 | expect(mock).toHaveBeenCalledWith('http://localhost/getSchema', { 9 | body: '{"schemaName":"MyType"}', 10 | headers: { 11 | Accept: 'application/json, text/plain, */*', 12 | 'Content-Type': 'application/json', 13 | }, 14 | method: 'post', 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/api/client/__tests__/get-screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { getScreenshot } from '../get-screenshot'; 2 | import fetch from 'jest-fetch-mock'; 3 | import { ScreenshotRequest, GetScreenshotResponse } from '../../typings'; 4 | 5 | describe('getSnapShot', () => { 6 | beforeEach(() => { 7 | fetch.doMock(); 8 | }); 9 | 10 | const reqData: ScreenshotRequest = { 11 | browserType: 'chromium', 12 | requestId: 'request-id', 13 | storyId: 'story-id', 14 | }; 15 | 16 | const respData: GetScreenshotResponse = { 17 | base64: 'base64', 18 | error: '', 19 | id: 'screenshot-id', 20 | }; 21 | 22 | it('should getScreenshot', async () => { 23 | fetch.mockResponseOnce(JSON.stringify(respData)); 24 | 25 | const data = await getScreenshot(reqData); 26 | 27 | expect(data).toStrictEqual(respData); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/api/client/__tests__/get-story-screenshots.test.ts: -------------------------------------------------------------------------------- 1 | import { getStoryScreenshots } from '../get-story-screenshots'; 2 | import fetch from 'jest-fetch-mock'; 3 | 4 | describe('getStoryScreenshots', () => { 5 | it('should have request payload', async () => { 6 | const mock = fetch.mockResponseOnce(JSON.stringify({})); 7 | await getStoryScreenshots({ fileName: 'story.ts', storyId: 'story-id' }); 8 | expect(mock).toHaveBeenCalledWith('http://localhost/screenshot/all', { 9 | body: '{"fileName":"story.ts","storyId":"story-id"}', 10 | headers: { 11 | Accept: 'application/json, text/plain, */*', 12 | 'Content-Type': 'application/json', 13 | }, 14 | method: 'post', 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/api/client/__tests__/save-screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { saveScreenshot } from '../save-screenshot'; 2 | import fetch from 'jest-fetch-mock'; 3 | import { SaveScreenshotRequest } from '../../typings'; 4 | describe('saveScreenshot', () => { 5 | it('should save', async () => { 6 | const mock = fetch.mockResponseOnce(JSON.stringify({})); 7 | await saveScreenshot({ 8 | base64: 'image', 9 | id: 'screenshot-id', 10 | } as SaveScreenshotRequest); 11 | expect(mock).toHaveBeenCalledWith('http://localhost/screenshot/save', { 12 | body: '{"base64":"image","id":"screenshot-id"}', 13 | headers: { 14 | Accept: 'application/json, text/plain, */*', 15 | 'Content-Type': 'application/json', 16 | }, 17 | method: 'post', 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/api/client/__tests__/test-screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { testScreenshot } from '../test-screenshot'; 2 | import fetch from 'jest-fetch-mock'; 3 | 4 | describe('testScreenshot', () => { 5 | it('should test', async () => { 6 | const mock = fetch.mockResponseOnce(JSON.stringify({})); 7 | await testScreenshot({ 8 | screenshotId: 'screenshot-id', 9 | storyId: 'story-id', 10 | }); 11 | expect(mock).toHaveBeenCalledWith('http://localhost/screenshot/test', { 12 | body: '{"screenshotId":"screenshot-id","storyId":"story-id"}', 13 | headers: { 14 | Accept: 'application/json, text/plain, */*', 15 | 'Content-Type': 'application/json', 16 | }, 17 | method: 'post', 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/api/client/__tests__/test-screenshots.test.ts: -------------------------------------------------------------------------------- 1 | import { testScreenshots } from '../test-screenshots'; 2 | import fetch from 'jest-fetch-mock'; 3 | 4 | describe('testScreenshots', () => { 5 | it('should teat', async () => { 6 | const mock = fetch.mockResponseOnce(JSON.stringify({})); 7 | await testScreenshots({ 8 | requestId: 'request-id', 9 | requestType: 'all', 10 | storyId: 'story-id', 11 | }); 12 | expect(mock).toHaveBeenCalledWith( 13 | 'http://localhost/screenshot/testScreenshots', 14 | { 15 | body: 16 | '{"requestId":"request-id","requestType":"all","storyId":"story-id"}', 17 | headers: { 18 | Accept: 'application/json, text/plain, */*', 19 | 'Content-Type': 'application/json', 20 | }, 21 | method: 'post', 22 | }, 23 | ); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/api/client/__tests__/test-story-screenshots.test.ts: -------------------------------------------------------------------------------- 1 | import { testStoryScreenshots } from '../test-story-screenshots'; 2 | import fetch from 'jest-fetch-mock'; 3 | 4 | describe('testStoryScreenshots', () => { 5 | it('should test', async () => { 6 | const mock = fetch.mockResponseOnce(JSON.stringify({})); 7 | await testStoryScreenshots({ 8 | fileName: 'story.ts', 9 | requestId: 'request-id', 10 | storyId: 'story-id', 11 | }); 12 | expect(mock).toHaveBeenCalledWith('http://localhost/screenshot/testStory', { 13 | body: 14 | '{"fileName":"story.ts","requestId":"request-id","storyId":"story-id"}', 15 | headers: { 16 | Accept: 'application/json, text/plain, */*', 17 | 'Content-Type': 'application/json', 18 | }, 19 | method: 'post', 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/api/client/__tests__/update-screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { updateScreenshot } from '../update-screenshot'; 2 | import fetch from 'jest-fetch-mock'; 3 | 4 | describe('deleteStoryScreenshots', () => { 5 | it('should deleteStoryScreenshots', async () => { 6 | const fetchMock = fetch.mockResponseOnce(''); 7 | 8 | await updateScreenshot({ 9 | screenshotId: 'screenshot-id', 10 | storyId: 'story-id', 11 | }); 12 | 13 | expect(fetchMock).toHaveBeenCalledWith( 14 | 'http://localhost/screenshot/update', 15 | { 16 | body: '{"screenshotId":"screenshot-id","storyId":"story-id"}', 17 | headers: { 18 | Accept: 'application/json, text/plain, */*', 19 | 'Content-Type': 'application/json', 20 | }, 21 | method: 'post', 22 | }, 23 | ); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/api/client/add-favourite-action.ts: -------------------------------------------------------------------------------- 1 | import { FavouriteActionSet } from '../../typings'; 2 | import { getEndpoint, responseHandler } from './utils'; 3 | 4 | export const addFavouriteAction = async (data: FavouriteActionSet) => { 5 | const restEndpoint = getEndpoint('ADD_FAVOURITE_ACTION'); 6 | 7 | await fetch(restEndpoint, { 8 | body: JSON.stringify(data), 9 | headers: { 10 | Accept: 'application/json, text/plain, */*', 11 | 'Content-Type': 'application/json', 12 | }, 13 | method: 'post', 14 | }).then(responseHandler); 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/client/change-screenshot-index.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { ChangeScreenshotIndex } from '../typings'; 3 | 4 | export const changeScreenShotIndex = async ( 5 | info: ChangeScreenshotIndex, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('CHANGE_SCREENSHOT_INDEX'); 8 | 9 | await fetch(restEndpoint, { 10 | body: JSON.stringify(info), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | }; 18 | -------------------------------------------------------------------------------- /src/api/client/delete-action-set.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { DeleteActionSetRequest } from '../typings'; 3 | 4 | export const deleteActionSet = async ( 5 | info: DeleteActionSetRequest, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('DELETE_ACTION_SET'); 8 | 9 | await fetch(restEndpoint, { 10 | body: JSON.stringify(info), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | }; 18 | -------------------------------------------------------------------------------- /src/api/client/delete-favourite-action.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { DeleteFavouriteAction } from '../typings'; 3 | 4 | export const deleteFavouriteAction = async ( 5 | info: DeleteFavouriteAction, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('DELETE_FAVOURITE_ACTION'); 8 | 9 | await fetch(restEndpoint, { 10 | body: JSON.stringify(info), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | }; 18 | -------------------------------------------------------------------------------- /src/api/client/delete-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | 3 | import { ScreenshotInfo } from '../../typings'; 4 | 5 | export const deleteScreenshot = async (info: ScreenshotInfo): Promise => { 6 | const restEndpoint = getEndpoint('DELETE_SCREENSHOT'); 7 | await fetch(restEndpoint, { 8 | body: JSON.stringify(info), 9 | headers: { 10 | Accept: 'application/json, text/plain, */*', 11 | 'Content-Type': 'application/json', 12 | }, 13 | method: 'post', 14 | }).then(responseHandler); 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/client/delete-story-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { StoryInfo } from '../../typings'; 3 | 4 | export const deleteStoryScreenshots = async ( 5 | info: StoryInfo, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('DELETE_STORY_SCREENSHOT'); 8 | await fetch(restEndpoint, { 9 | body: JSON.stringify(info), 10 | headers: { 11 | Accept: 'application/json, text/plain, */*', 12 | 'Content-Type': 'application/json', 13 | }, 14 | method: 'post', 15 | }).then(responseHandler); 16 | }; 17 | -------------------------------------------------------------------------------- /src/api/client/fix-screenshot-file-name.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { FixScreenshotFileName } from '../typings'; 3 | 4 | export const fixScreenshotFileName = async ( 5 | info: FixScreenshotFileName, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('FIX_STORY_TITLE_CHANGE'); 8 | 9 | const resp = await fetch(restEndpoint, { 10 | body: JSON.stringify(info), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | 18 | return resp; 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/client/get-action-schema.ts: -------------------------------------------------------------------------------- 1 | import { ActionSchemaList } from '../../typings'; 2 | import { getEndpoint, responseHandler } from './utils'; 3 | 4 | export const getActionSchema = async (): Promise => { 5 | const restEndpoint = getEndpoint('GET_ACTIONS_DATA'); 6 | 7 | const data = await fetch(restEndpoint, { 8 | headers: { 9 | Accept: 'application/json, text/plain, */*', 10 | 'Content-Type': 'application/json', 11 | }, 12 | method: 'post', 13 | }).then(responseHandler); 14 | 15 | return data; 16 | }; 17 | -------------------------------------------------------------------------------- /src/api/client/get-action-set.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { StoryInfo, ActionSet } from '../../typings'; 3 | 4 | export const getActionSet = async ( 5 | info: StoryInfo, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('GET_ACTION_SET'); 8 | 9 | const resp = await fetch(restEndpoint, { 10 | body: JSON.stringify(info), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | 18 | return resp; 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/client/get-favourite-actions.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { FavouriteActionSet } from '../../typings'; 3 | 4 | export const getFavouriteActions = async (): Promise => { 5 | const restEndpoint = getEndpoint('GET_FAVOURITE_ACTIONS'); 6 | 7 | const resp = await fetch(restEndpoint, { 8 | headers: { 9 | Accept: 'application/json, text/plain, */*', 10 | 'Content-Type': 'application/json', 11 | }, 12 | method: 'post', 13 | }).then(responseHandler); 14 | 15 | return resp; 16 | }; 17 | -------------------------------------------------------------------------------- /src/api/client/get-schema-client.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { Definition } from 'ts-to-json/dist/src/Schema/Definition'; 3 | 4 | export const getSchemaClient = async ( 5 | schemaName: string, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('GET_SCHEMA'); 8 | 9 | const data = await fetch(restEndpoint, { 10 | body: JSON.stringify({ schemaName: schemaName }), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | 18 | return data; 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/client/get-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotRequest, GetScreenshotResponse } from '../typings'; 2 | import { getEndpoint, responseHandler } from './utils'; 3 | 4 | export const getScreenshot = async ( 5 | options: ScreenshotRequest, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('TAKE_SCREENSHOT'); 8 | 9 | const data = await fetch(restEndpoint, { 10 | body: JSON.stringify(options), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | 18 | return data; 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/client/get-story-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { StoryInfo } from '../../typings'; 3 | 4 | export const getStoryScreenshots = async (data: StoryInfo) => { 5 | const restEndpoint = getEndpoint('GET_STORY_SCREENSHOTS'); 6 | 7 | const result = await fetch(restEndpoint, { 8 | body: JSON.stringify(data), 9 | headers: { 10 | Accept: 'application/json, text/plain, */*', 11 | 'Content-Type': 'application/json', 12 | }, 13 | method: 'post', 14 | }).then(responseHandler); 15 | 16 | return result; 17 | }; 18 | -------------------------------------------------------------------------------- /src/api/client/get-theme-data.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@material-ui/core'; 2 | import { getEndpoint, responseHandler } from './utils'; 3 | 4 | export const getThemeData = async (): Promise => { 5 | const restEndpoint = getEndpoint('GET_THEME'); 6 | 7 | const resp = await fetch(restEndpoint, { 8 | headers: { 9 | Accept: 'application/json, text-plain, */*', 10 | 'Content-Type': 'application/json', 11 | }, 12 | method: 'post', 13 | }).then(responseHandler); 14 | 15 | return resp; 16 | }; 17 | -------------------------------------------------------------------------------- /src/api/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './get-action-schema'; 3 | export * from './get-screenshot'; 4 | export * from './save-action-set'; 5 | export * from './save-screenshot'; 6 | export * from './delete-action-set'; 7 | export * from './delete-screenshot'; 8 | export * from './test-screenshot'; 9 | export * from './test-story-screenshots'; 10 | export * from './test-screenshots'; 11 | export * from './update-screenshot'; 12 | export * from './delete-story-screenshots'; 13 | export * from './change-screenshot-index'; 14 | export * from './add-favourite-action'; 15 | -------------------------------------------------------------------------------- /src/api/client/save-action-set.ts: -------------------------------------------------------------------------------- 1 | import { SaveActionSetRequest } from '..//typings'; 2 | import { getEndpoint, responseHandler } from './utils'; 3 | 4 | export const saveActionSet = async (data: SaveActionSetRequest) => { 5 | const restEndpoint = getEndpoint('SAVE_ACTION_SET'); 6 | 7 | await fetch(restEndpoint, { 8 | body: JSON.stringify(data), 9 | headers: { 10 | Accept: 'application/json, text/plain, */*', 11 | 'Content-Type': 'application/json', 12 | }, 13 | method: 'post', 14 | }).then(responseHandler); 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/client/save-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { SaveScreenshotRequest, ImageDiffResult } from '../typings'; 2 | import { getEndpoint, responseHandler } from './utils'; 3 | 4 | export const saveScreenshot = async ( 5 | data: SaveScreenshotRequest, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('SAVE_SCREENSHOT'); 8 | 9 | const resData = await fetch(restEndpoint, { 10 | body: JSON.stringify(data), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | 18 | return resData; 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/client/test-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { ImageDiffResult } from '../typings'; 3 | import { ScreenshotInfo } from '../../typings'; 4 | 5 | export const testScreenshot = async ( 6 | data: ScreenshotInfo, 7 | ): Promise => { 8 | const restEndpoint = getEndpoint('TEST_SCREENSHOT'); 9 | const resp = await fetch(restEndpoint, { 10 | body: JSON.stringify(data), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | 18 | return resp; 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/client/test-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { ImageDiffResult, TestScreenShots } from '../typings'; 3 | 4 | export const testScreenshots = async ( 5 | data: TestScreenShots, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('TEST_SCREENSHOTS'); 8 | 9 | const resp = await fetch(restEndpoint, { 10 | body: JSON.stringify(data), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | 18 | return resp; 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/client/test-story-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { StoryInfo } from '../../typings'; 3 | import { ImageDiffResult } from '../typings'; 4 | import { RequestData } from '../../typings/request'; 5 | 6 | export const testStoryScreenshots = async ( 7 | data: StoryInfo & RequestData, 8 | ): Promise => { 9 | const restEndpoint = getEndpoint('TEST_STORY_SCREENSHOT'); 10 | const resp = await fetch(restEndpoint, { 11 | body: JSON.stringify(data), 12 | headers: { 13 | Accept: 'application/json, text/plain, */*', 14 | 'Content-Type': 'application/json', 15 | }, 16 | method: 'post', 17 | }).then(responseHandler); 18 | 19 | return resp; 20 | }; 21 | -------------------------------------------------------------------------------- /src/api/client/update-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint, responseHandler } from './utils'; 2 | import { ImageDiffResult, UpdateScreenshot } from '../typings'; 3 | 4 | export const updateScreenshot = async ( 5 | data: UpdateScreenshot, 6 | ): Promise => { 7 | const restEndpoint = getEndpoint('UPDATE_SCREENSHOT'); 8 | 9 | const resp = await fetch(restEndpoint, { 10 | body: JSON.stringify(data), 11 | headers: { 12 | Accept: 'application/json, text/plain, */*', 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'post', 16 | }).then(responseHandler); 17 | 18 | return resp; 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/client/utils/__tests__/endpoint.test.ts: -------------------------------------------------------------------------------- 1 | import { getEndpoint } from '../endpoint'; 2 | 3 | describe('getEndpoint', () => { 4 | it('should return endpoint', () => { 5 | expect(getEndpoint('DELETE_ACTION_SET')).toBe( 6 | 'http://localhost/actionSet/delete', 7 | ); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/api/client/utils/endpoint.ts: -------------------------------------------------------------------------------- 1 | import { ROUTE } from '../../../constants/routes'; 2 | 3 | export const getEndpoint = (route: keyof typeof ROUTE) => { 4 | const url = `${window.location.protocol}//${window.location.host}`; 5 | return url + ROUTE[route]; 6 | }; 7 | -------------------------------------------------------------------------------- /src/api/client/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './endpoint'; 2 | export * from './response-handler'; 3 | -------------------------------------------------------------------------------- /src/api/client/utils/response-handler.ts: -------------------------------------------------------------------------------- 1 | export const responseHandler = async (res: Partial) => { 2 | let resp; 3 | try { 4 | resp = await res.json(); 5 | } catch { 6 | return this; 7 | } 8 | if (!res.ok) { 9 | throw new Error(resp.message); 10 | } 11 | return resp; 12 | }; 13 | -------------------------------------------------------------------------------- /src/api/server/__mocks__/configs.ts: -------------------------------------------------------------------------------- 1 | import { defaultConfigs } from '../../../../__test_data__/configs'; 2 | 3 | const getConfigs = jest.fn(); 4 | 5 | getConfigs.mockImplementation(() => { 6 | return defaultConfigs(); 7 | }); 8 | 9 | export { getConfigs }; 10 | -------------------------------------------------------------------------------- /src/api/server/configs.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../typings'; 2 | import { Page } from 'playwright'; 3 | import { migration } from './migration'; 4 | 5 | let configs: Config; 6 | 7 | export const setConfig = (conf: Config) => { 8 | configs = { 9 | ...(conf as unknown as Config), 10 | concurrencyLimit: { 11 | file: 1, 12 | story: 1, 13 | ...conf.concurrencyLimit, 14 | }, 15 | }; 16 | if (configs.enableMigration) { 17 | migration(); 18 | } 19 | }; 20 | 21 | export const getConfigs = () => { 22 | if (!configs) { 23 | throw new Error('Configuration has not been set.'); 24 | } 25 | 26 | return configs; 27 | }; 28 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/add-to-favourite.test.ts: -------------------------------------------------------------------------------- 1 | import { addToFavourite } from '../add-to-favourite'; 2 | import { Request, Response } from 'express'; 3 | 4 | jest.mock('../../services/add-to-favourite', () => ({ 5 | addToFavourite: jest.fn(), 6 | })); 7 | 8 | describe('addToFavourite', () => { 9 | it('should be defined', () => { 10 | expect(addToFavourite).toBeDefined(); 11 | }); 12 | 13 | it('should send 200 status', async () => { 14 | const statusMock = jest.fn(); 15 | const endMock = jest.fn(); 16 | await addToFavourite( 17 | {} as Request, 18 | { end: endMock, status: statusMock } as unknown as Response, 19 | ); 20 | expect(statusMock).toHaveBeenCalledWith(200); 21 | expect(endMock).toHaveBeenCalledTimes(1); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/change-screenshot-index.test.ts: -------------------------------------------------------------------------------- 1 | import { changeScreenShotIndex } from '../change-screenshot-index'; 2 | import { changeScreenshotIndex as changeScreenshotIndexService } from '../../services/change-screenshot-index'; 3 | import { Response, Request } from 'express'; 4 | 5 | jest.mock('../../services/change-screenshot-index.ts'); 6 | 7 | describe('changeScreenShotIndex', () => { 8 | it('should send 200', async () => { 9 | const statusMock = jest.fn(); 10 | const endMock = jest.fn(); 11 | await changeScreenShotIndex( 12 | {} as Request, 13 | ({ end: endMock, status: statusMock } as unknown) as Response, 14 | ); 15 | expect(statusMock).toHaveBeenCalledWith(200); 16 | expect(endMock).toHaveBeenCalledTimes(1); 17 | expect(changeScreenshotIndexService).toHaveBeenCalledTimes(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/delete-action-set.test.ts: -------------------------------------------------------------------------------- 1 | import { deleteActionSet } from '../delete-action-set'; 2 | import { Response, Request } from 'express'; 3 | 4 | jest.mock('../../services/delete-action-set', () => ({ 5 | deleteActionSet: jest.fn(), 6 | })); 7 | 8 | describe('deleteActionSet', () => { 9 | const statusMock = jest.fn(); 10 | const endMock = jest.fn(); 11 | 12 | it('should send 200', async () => { 13 | await deleteActionSet( 14 | {} as Request, 15 | ({ end: endMock, status: statusMock } as unknown) as Response, 16 | ); 17 | expect(statusMock).toHaveBeenCalledWith(200); 18 | expect(endMock).toHaveBeenCalledTimes(1); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/delete-favourite-action.test.ts: -------------------------------------------------------------------------------- 1 | import { deleteFavouriteAction } from '../delete-favourite-action'; 2 | import { Response, Request } from 'express'; 3 | 4 | jest.mock('../../services/delete-favourite-action', () => ({ 5 | deleteFavouriteAction: jest.fn(), 6 | })); 7 | 8 | describe('deleteFavouriteAction', () => { 9 | const statusMock = jest.fn(); 10 | const endMock = jest.fn(); 11 | 12 | it('should be defined', () => { 13 | expect(deleteFavouriteAction).toBeDefined(); 14 | }); 15 | 16 | it('should send 200', async () => { 17 | await deleteFavouriteAction( 18 | {} as Request, 19 | { end: endMock, status: statusMock } as unknown as Response, 20 | ); 21 | expect(statusMock).toHaveBeenCalledWith(200); 22 | expect(endMock).toHaveBeenCalledTimes(1); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/delete-screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { deleteScreenshot } from '../delete-screenshot'; 2 | 3 | jest.mock('../../services/delete-screenshot.ts'); 4 | 5 | describe('deleteScreenshot', () => { 6 | const statusMock = jest.fn(); 7 | const endMock = jest.fn(); 8 | 9 | it('should send 200', async () => { 10 | await deleteScreenshot( 11 | {} as Request, 12 | ({ end: endMock, status: statusMock } as unknown) as Response, 13 | ); 14 | expect(statusMock).toHaveBeenCalledWith(200); 15 | expect(endMock).toHaveBeenCalledTimes(1); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/delete-story-screenshots.test.ts: -------------------------------------------------------------------------------- 1 | import { deleteStoryScreenshot } from '../delete-story-screenshots'; 2 | import { deleteStoryScreenshots as deleteStoryScreenshotsService } from '../../services/delete-story-screenshots'; 3 | 4 | jest.mock('../../services/delete-story-screenshots.ts'); 5 | 6 | describe('deleteStoryScreenshot', () => { 7 | it('should send 200', async () => { 8 | const statusMock = jest.fn(); 9 | const endMock = jest.fn(); 10 | await deleteStoryScreenshot( 11 | {} as Request, 12 | ({ end: endMock, status: statusMock } as unknown) as Response, 13 | ); 14 | expect(statusMock).toHaveBeenCalledWith(200); 15 | expect(endMock).toHaveBeenCalledTimes(1); 16 | expect(deleteStoryScreenshotsService).toHaveBeenCalledTimes(1); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/fix-screenshot-file-name.test.ts: -------------------------------------------------------------------------------- 1 | import { fixScreenshotFileName } from '../fix-screenshot-file-name'; 2 | 3 | jest.mock('../../services/fix-screenshot-file-name'); 4 | 5 | describe('fixScreenshotFileName', () => { 6 | it('should be defined', () => { 7 | expect(fixScreenshotFileName).toBeDefined(); 8 | }); 9 | 10 | it('should handle data', async () => { 11 | const jsonMock = jest.fn(); 12 | await fixScreenshotFileName( 13 | {} as Request, 14 | { json: jsonMock } as Partial, 15 | ); 16 | expect(jsonMock).toHaveBeenCalledTimes(1); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/get-actions-schema.test.ts: -------------------------------------------------------------------------------- 1 | import { getActionsSchema } from '../get-actions-schema'; 2 | import { Request, Response } from 'express'; 3 | import { getActionSchemaData } from '../../../../../__test_data__'; 4 | 5 | jest.mock('../../services/get-actions-schema', () => ({ 6 | getActionsSchema: () => getActionSchemaData(), 7 | })); 8 | 9 | describe('getActionsSchema', () => { 10 | it('should send schema', async () => { 11 | const jsonMock = jest.fn(); 12 | await getActionsSchema( 13 | {} as Request, 14 | { json: jsonMock } as Partial, 15 | ); 16 | expect(jsonMock).toHaveBeenCalledWith(getActionSchemaData()); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/get-schema-controller.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../../../__manual_mocks__/ts-to-json'; 2 | import { getSchemaController } from '../get-schema-controller'; 3 | import * as getSchema from '../../services/get-schema-service'; 4 | import { Request, Response } from 'express'; 5 | 6 | describe('getSchemaController', () => { 7 | it('should send schema', async () => { 8 | const jsonMock = jest.fn(); 9 | const spy = jest.spyOn(getSchema, 'getSchemaService'); 10 | await getSchemaController( 11 | { body: { schemaName: 'MyType' } } as Request, 12 | ({ 13 | json: jsonMock, 14 | } as unknown) as Response, 15 | ); 16 | expect(spy.mock.calls[0][0]).toBe('MyType'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/get-screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { getScreenshot } from '../get-screenshot'; 2 | import { Request, Response } from 'express'; 3 | 4 | jest.mock('../../services/make-screenshot', () => ({ 5 | makeScreenshot: () => ({ 6 | base64: 'base64', 7 | }), 8 | })); 9 | 10 | describe('getScreenshot', () => { 11 | it('should send base64', async () => { 12 | const jsonMock = jest.fn(); 13 | await getScreenshot( 14 | { 15 | headers: { 16 | host: 'localhost', 17 | }, 18 | } as Request, 19 | ({ 20 | json: jsonMock, 21 | } as unknown) as Response, 22 | ); 23 | expect(jsonMock).toHaveBeenCalledWith({ 24 | base64: 'base64', 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/get-theme-data.test.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@material-ui/core/styles'; 2 | import { getThemeData } from '../get-theme-data'; 3 | import { Request, Response } from 'express'; 4 | 5 | const theme = createTheme({ 6 | palette: { 7 | primary: { 8 | main: '#00FF00', 9 | }, 10 | }, 11 | }); 12 | 13 | jest.mock('../../services/get-theme-data', () => ({ 14 | getThemeData: () => theme, 15 | })); 16 | 17 | describe('getThemeData', () => { 18 | it('should send theme data', async () => { 19 | const jsonMock = jest.fn(); 20 | await getThemeData({} as Request, { json: jsonMock } as Partial); 21 | 22 | expect(jsonMock).toHaveBeenCalledWith(theme); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/save-action-set.test.ts: -------------------------------------------------------------------------------- 1 | import { saveActionSet } from '../save-action-set'; 2 | import { Request, Response } from 'express'; 3 | 4 | jest.mock('../../services/save-action-set', () => ({ 5 | saveActionSet: jest.fn(), 6 | })); 7 | 8 | describe('saveActionSet', () => { 9 | it('should send 200 status', async () => { 10 | const statusMock = jest.fn(); 11 | const endMock = jest.fn(); 12 | await saveActionSet( 13 | {} as Request, 14 | ({ end: endMock, status: statusMock } as unknown) as Response, 15 | ); 16 | expect(statusMock).toHaveBeenCalledWith(200); 17 | expect(endMock).toHaveBeenCalledTimes(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/save-screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { saveScreenshot } from '../save-screenshot'; 2 | import { Request, Response } from 'express'; 3 | import { SaveScreenshotRequest } from '../../../typings'; 4 | 5 | jest.mock('../../services/save-screenshot.ts'); 6 | 7 | describe('saveScreenshot', () => { 8 | it('should return result', async () => { 9 | const jsonMock = jest.fn(); 10 | 11 | await saveScreenshot( 12 | { body: { id: 'screenshot-id' } as SaveScreenshotRequest } as Request, 13 | ({ json: jsonMock } as unknown) as Response, 14 | ); 15 | expect(jsonMock).toHaveBeenCalledWith({ 16 | pass: true, 17 | screenshotId: 'screenshot-id', 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/test-screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { testScreenshot } from '../test-screenshot'; 2 | import { Request, Response } from 'express'; 3 | import { ScreenshotInfo } from '../../../../typings'; 4 | 5 | jest.mock('../../services/test-screenshot-service.ts'); 6 | 7 | describe('testScreenshot', () => { 8 | it('should have result', async () => { 9 | const jsonMock = jest.fn(); 10 | 11 | await testScreenshot( 12 | { 13 | body: { screenshotId: 'screenshot-id' } as ScreenshotInfo, 14 | headers: { host: 'localhost' }, 15 | } as Request, 16 | ({ json: jsonMock } as unknown) as Response, 17 | ); 18 | expect(jsonMock).toHaveBeenCalledWith({ 19 | pass: true, 20 | screenshotId: 'screenshot-id', 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/test-screenshots.test.ts: -------------------------------------------------------------------------------- 1 | import { testScreenshots } from '../test-screenshots'; 2 | import { Request, Response } from 'express'; 3 | 4 | jest.mock('../../services/test-screenshots-service.ts'); 5 | 6 | describe('testScreenshots', () => { 7 | it('should return result', async () => { 8 | const jsonMock = jest.fn(); 9 | 10 | await testScreenshots( 11 | ({ headers: { host: 'localhost' } } as unknown) as Request, 12 | ({ json: jsonMock } as unknown) as Response, 13 | ); 14 | expect(jsonMock).toHaveBeenCalledWith([ 15 | { pass: true, screenshotId: 'screenshot-id' }, 16 | ]); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/server/controller/__tests__/test-story-screenshots.test.ts: -------------------------------------------------------------------------------- 1 | import { testStoryScreenshots } from '../test-story-screenshots'; 2 | import { Request, Response } from 'express'; 3 | import { StoryInfo } from '../../../../typings'; 4 | 5 | jest.mock('../../services/test-story-screenshots.ts'); 6 | 7 | describe('testStoryScreenshots', () => { 8 | it('should have result', async () => { 9 | const jsonMock = jest.fn(); 10 | 11 | await testStoryScreenshots( 12 | { 13 | body: { fileName: 'file-name' } as StoryInfo, 14 | headers: { host: 'localhost' }, 15 | } as Request, 16 | ({ json: jsonMock } as unknown) as Response, 17 | ); 18 | expect(jsonMock).toHaveBeenCalledWith([ 19 | { pass: true, screenshotId: 'screenshot-id' }, 20 | ]); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/api/server/controller/add-to-favourite.ts: -------------------------------------------------------------------------------- 1 | import { addToFavourite as addToFavouriteService } from '../services/add-to-favourite'; 2 | import { Request, Response } from 'express'; 3 | import { FavouriteActionSet } from '../../../typings'; 4 | 5 | export const addToFavourite = async ( 6 | req: Request, 7 | res: Response, 8 | ): Promise => { 9 | const reqData = req.body as FavouriteActionSet; 10 | 11 | await addToFavouriteService(reqData); 12 | 13 | res.status(200); 14 | res.end(); 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/server/controller/change-screenshot-index.ts: -------------------------------------------------------------------------------- 1 | import { changeScreenshotIndex as changeScreenShotIndexClient } from '../services/change-screenshot-index'; 2 | import { ChangeScreenshotIndex } from '../../typings'; 3 | import { Response, Request } from 'express'; 4 | 5 | export const changeScreenShotIndex = async ( 6 | req: Request, 7 | res: Response, 8 | ): Promise => { 9 | const reqData = req.body as ChangeScreenshotIndex; 10 | await changeScreenShotIndexClient(reqData); 11 | res.status(200); 12 | res.end(); 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/server/controller/delete-action-set.ts: -------------------------------------------------------------------------------- 1 | import { deleteActionSet as deleteActionSetService } from '../services/delete-action-set'; 2 | import { DeleteActionSetRequest } from '../../typings'; 3 | import { Response, Request } from 'express'; 4 | 5 | export const deleteActionSet = async ( 6 | req: Request, 7 | res: Response, 8 | ): Promise => { 9 | const reqData = req.body as DeleteActionSetRequest; 10 | await deleteActionSetService(reqData); 11 | res.status(200); 12 | res.end(); 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/server/controller/delete-favourite-action.ts: -------------------------------------------------------------------------------- 1 | import { DeleteFavouriteAction } from '../../typings/delete-favourite-action'; 2 | import { deleteFavouriteAction as deleteFavouriteActionService } from '../services/delete-favourite-action'; 3 | 4 | export const deleteFavouriteAction = async (req, res): Promise => { 5 | const reqData = req.body as DeleteFavouriteAction; 6 | await deleteFavouriteActionService(reqData); 7 | res.status(200); 8 | res.end(); 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/server/controller/delete-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotInfo } from '../../../typings'; 2 | import { deleteScreenshot as deleteScreenshotService } from '../services/delete-screenshot'; 3 | 4 | export const deleteScreenshot = async (req, res): Promise => { 5 | const reqData = req.body as ScreenshotInfo; 6 | await deleteScreenshotService(reqData); 7 | res.status(200); 8 | res.end(); 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/server/controller/delete-story-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { StoryInfo } from '../../../typings'; 2 | import { deleteStoryScreenshots as deleteStoryScreenshotsService } from '../services/delete-story-screenshots'; 3 | 4 | export const deleteStoryScreenshot = async (req, res): Promise => { 5 | const reqData = req.body as StoryInfo; 6 | await deleteStoryScreenshotsService(reqData); 7 | res.status(200); 8 | res.end(); 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/server/controller/fix-screenshot-file-name.ts: -------------------------------------------------------------------------------- 1 | import { fixScreenshotFileName as service } from '../services/fix-screenshot-file-name'; 2 | import { FixScreenshotFileName } from '../../typings'; 3 | 4 | export const fixScreenshotFileName = async (req, res): Promise => { 5 | const reqData = req.body as FixScreenshotFileName; 6 | const actionSets = await service(reqData); 7 | res.json(actionSets); 8 | }; 9 | -------------------------------------------------------------------------------- /src/api/server/controller/get-action-set.ts: -------------------------------------------------------------------------------- 1 | import { getActionSet as getActionSetService } from '../services/get-action-set'; 2 | import { StoryInfo } from '../../../typings'; 3 | 4 | export const getActionSet = async (req, res): Promise => { 5 | const reqData = req.body as StoryInfo; 6 | const actionSets = await getActionSetService(reqData); 7 | res.json(actionSets); 8 | }; 9 | -------------------------------------------------------------------------------- /src/api/server/controller/get-actions-schema.ts: -------------------------------------------------------------------------------- 1 | import { getActionsSchema as getActionsSchemaService } from '../services/get-actions-schema'; 2 | 3 | export const getActionsSchema = async (_req, res): Promise => { 4 | const actions = getActionsSchemaService(); 5 | res.json(actions); 6 | }; 7 | -------------------------------------------------------------------------------- /src/api/server/controller/get-favourite-actions.ts: -------------------------------------------------------------------------------- 1 | import { getFavouriteActions as getFavouriteActionsService } from '../services/get-favourite-actions'; 2 | import { Request, Response } from 'express'; 3 | 4 | export const getFavouriteActions = async ( 5 | _req: Request, 6 | res: Response, 7 | ): Promise => { 8 | const result = await getFavouriteActionsService(); 9 | 10 | res.json(result); 11 | }; 12 | -------------------------------------------------------------------------------- /src/api/server/controller/get-schema-controller.ts: -------------------------------------------------------------------------------- 1 | import { getSchemaService, SchemaName } from '../services/get-schema-service'; 2 | import { Request, Response } from 'express'; 3 | 4 | export const getSchemaController = async ( 5 | req: Request, 6 | res: Response, 7 | ): Promise => { 8 | const reqData = req.body.schemaName as SchemaName; 9 | 10 | const result = getSchemaService(reqData); 11 | 12 | res.json({ ...result }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/server/controller/get-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { makeScreenshot } from '../services/make-screenshot'; 2 | import { ScreenshotRequest, GetScreenshotResponse } from '../../typings'; 3 | import { Request, Response } from 'express'; 4 | 5 | export const getScreenshot = async ( 6 | req: Request, 7 | res: Response, 8 | ): Promise => { 9 | const reqData = req.body as ScreenshotRequest; 10 | 11 | const snapshotData = await makeScreenshot(reqData, true); 12 | 13 | res.json({ base64: snapshotData.base64 } as GetScreenshotResponse); 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/server/controller/get-story-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { getStoryScreenshotsData as getStoryScreenshotsService } from '../services/get-story-screenshots-data'; 2 | import { Request, Response } from 'express'; 3 | import { StoryInfo } from '../../../typings'; 4 | 5 | export const getStoryScreenshots = async ( 6 | req: Request, 7 | res: Response, 8 | ): Promise => { 9 | const reqData = req.body as StoryInfo; 10 | 11 | const snapshotData = await getStoryScreenshotsService(reqData); 12 | 13 | res.json(snapshotData); 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/server/controller/get-theme-data.ts: -------------------------------------------------------------------------------- 1 | import { getThemeData as getThemeDataService} from "../services/get-theme-data"; 2 | 3 | export const getThemeData = async (_req, res): Promise => { 4 | const theme = getThemeDataService(); 5 | res.json(theme); 6 | } -------------------------------------------------------------------------------- /src/api/server/controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-to-favourite'; 2 | export * from './change-screenshot-index'; 3 | export * from './delete-action-set'; 4 | export * from './delete-favourite-action'; 5 | export * from './delete-screenshot'; 6 | export * from './delete-story-screenshots'; 7 | export * from './fix-screenshot-file-name'; 8 | export * from './get-action-set'; 9 | export * from './get-actions-schema'; 10 | export * from './get-favourite-actions'; 11 | export * from './get-schema-controller'; 12 | export * from './get-screenshot'; 13 | export * from './get-story-screenshots'; 14 | export * from './get-theme-data'; 15 | export * from './save-action-set'; 16 | export * from './save-screenshot'; 17 | export * from './test-screenshot'; 18 | export * from './test-screenshots'; 19 | export * from './test-story-screenshots'; 20 | export * from './update-screenshot'; 21 | -------------------------------------------------------------------------------- /src/api/server/controller/save-action-set.ts: -------------------------------------------------------------------------------- 1 | import { saveActionSet as saveActionSetService } from '../services/save-action-set'; 2 | import { SaveActionSetRequest } from '../../typings'; 3 | import { Request, Response } from 'express'; 4 | 5 | export const saveActionSet = async ( 6 | req: Request, 7 | res: Response, 8 | ): Promise => { 9 | const reqData = req.body as SaveActionSetRequest; 10 | await saveActionSetService(reqData); 11 | res.status(200); 12 | res.end(); 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/server/controller/save-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { saveScreenshot as saveScreenshotService } from '../services/save-screenshot'; 2 | import { SaveScreenshotRequest } from '../../typings'; 3 | import { Request, Response } from 'express'; 4 | 5 | export const saveScreenshot = async ( 6 | req: Request, 7 | res: Response, 8 | ): Promise => { 9 | const reqData = req.body as SaveScreenshotRequest; 10 | 11 | const result = await saveScreenshotService(reqData); 12 | 13 | res.json(result); 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/server/controller/test-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { testScreenshotService } from '../services/test-screenshot-service'; 2 | import { Request, Response } from 'express'; 3 | import { ScreenshotInfo } from '../../../typings'; 4 | import { RequestData } from '../../../typings/request'; 5 | 6 | export const testScreenshot = async ( 7 | req: Request, 8 | res: Response, 9 | ): Promise => { 10 | const reqData = req.body as ScreenshotInfo & RequestData; 11 | 12 | const snapshotData = await testScreenshotService(reqData); 13 | 14 | res.json(snapshotData); 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/server/controller/test-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { testScreenshots as testAppScreenshotService } from '../services/test-screenshots-service'; 2 | import { Request, Response } from 'express'; 3 | import { TestScreenShots } from '../../typings'; 4 | 5 | export const testScreenshots = async ( 6 | req: Request, 7 | res: Response, 8 | ): Promise => { 9 | const reqData = req.body as TestScreenShots; 10 | const snapshotData = await testAppScreenshotService(reqData); 11 | 12 | res.json(snapshotData); 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/server/controller/test-story-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { testStoryScreenshots as testStoryScreenshotService } from '../services/test-story-screenshots'; 2 | import { Request, Response } from 'express'; 3 | import { StoryInfo } from '../../../typings'; 4 | import { RequestData } from '../../../typings/request'; 5 | 6 | export const testStoryScreenshots = async ( 7 | req: Request, 8 | res: Response, 9 | ): Promise => { 10 | const reqData = req.body as StoryInfo & RequestData; 11 | 12 | const snapshotData = await testStoryScreenshotService(reqData); 13 | 14 | res.json(snapshotData); 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/server/controller/update-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { updateScreenshotService } from '../services/update-screenshot-service'; 2 | import { Request, Response } from 'express'; 3 | import { UpdateScreenshot } from '../../typings'; 4 | 5 | export const updateScreenshot = async ( 6 | req: Request, 7 | res: Response, 8 | ): Promise => { 9 | const reqData = req.body as UpdateScreenshot; 10 | 11 | const snapshotData = await updateScreenshotService(reqData); 12 | 13 | res.json(snapshotData); 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/server/migration/__mocks__/migration.ts: -------------------------------------------------------------------------------- 1 | export const migration = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/api/server/migration/index.ts: -------------------------------------------------------------------------------- 1 | export * from './migration'; 2 | -------------------------------------------------------------------------------- /src/api/server/migration/migration-v3.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightData } from '../../../typings'; 2 | 3 | export const migrationV3 = (data: PlaywrightData, version: string) => { 4 | data.version = version; 5 | return data; 6 | }; 7 | -------------------------------------------------------------------------------- /src/api/server/migration/migration-v4.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightData } from '../../../typings'; 2 | 3 | export const migrationV4 = (data: PlaywrightData, version: string) => { 4 | data.version = version; 5 | return data; 6 | }; 7 | -------------------------------------------------------------------------------- /src/api/server/scripts/__tests__/generate-schema.test.ts: -------------------------------------------------------------------------------- 1 | import { createProgramMock } from '../../../../../__manual_mocks__/ts-to-json'; 2 | import { generateSchema } from '../generate-schema'; 3 | 4 | import { resolve } from 'path'; 5 | 6 | describe('generateSchema', () => { 7 | const path = resolve('./src/api/server/services/typings/app-types.ts'); 8 | 9 | it('should return properties', () => { 10 | expect(generateSchema({ path, type: 'MyType' })).toStrictEqual({ 11 | props: true, 12 | }); 13 | }); 14 | 15 | it('should have path', () => { 16 | generateSchema({ path, type: 'MyType' }); 17 | expect(createProgramMock.mock.calls[0][0].path).toBe(path); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/change-screenshot-index.ts: -------------------------------------------------------------------------------- 1 | const changeScreenshotIndex = jest.fn(); 2 | 3 | export { changeScreenshotIndex }; 4 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/delete-action-set.ts: -------------------------------------------------------------------------------- 1 | const deleteActionSet = jest.fn(); 2 | 3 | export { deleteActionSet }; 4 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/delete-screenshot.ts: -------------------------------------------------------------------------------- 1 | export const deleteScreenshot = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/delete-story-screenshots.ts: -------------------------------------------------------------------------------- 1 | const deleteStoryScreenshots = jest.fn(); 2 | 3 | export { deleteStoryScreenshots }; 4 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/diff-image-to-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { ImageDiffResult } from '../../../typings'; 2 | 3 | const diffImageToScreenshot = jest.fn(); 4 | 5 | diffImageToScreenshot.mockImplementation( 6 | (): ImageDiffResult => ({ 7 | added: true, 8 | }), 9 | ); 10 | 11 | export { diffImageToScreenshot }; 12 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/get-action-set.ts: -------------------------------------------------------------------------------- 1 | const getActionSet = jest.fn(); 2 | 3 | export { getActionSet }; 4 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/get-actions-schema.ts: -------------------------------------------------------------------------------- 1 | const getActionsSchema = jest.fn(); 2 | 3 | export { getActionsSchema }; 4 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/get-story-screenshots-data.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotData, StoryInfo } from '../../../../typings'; 2 | 3 | const getStoryScreenshotsData = jest.fn(); 4 | 5 | getStoryScreenshotsData.mockImplementation( 6 | (data: StoryInfo): Promise => { 7 | return new Promise((resolve) => { 8 | resolve([ 9 | { 10 | browserType: 'chromium', 11 | id: 'screenshot-id', 12 | title: data.fileName + '-screenshot', 13 | }, 14 | ]); 15 | }); 16 | }, 17 | ); 18 | 19 | export { getStoryScreenshotsData }; 20 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/get-theme-data.ts: -------------------------------------------------------------------------------- 1 | const getThemeData = jest.fn(); 2 | 3 | export { getThemeData }; -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/make-screenshot.ts: -------------------------------------------------------------------------------- 1 | const makeScreenshot = jest.fn(); 2 | makeScreenshot.mockImplementation(() => { 3 | return new Promise((resolve) => { 4 | resolve({ 5 | base64: 'base64-image', 6 | browserName: '', 7 | buffer: Buffer.from('base64-image', 'utf-8'), 8 | }); 9 | }); 10 | }); 11 | 12 | export { makeScreenshot }; 13 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/save-action-set.ts: -------------------------------------------------------------------------------- 1 | const saveActionSet = jest.fn(); 2 | 3 | export { saveActionSet }; 4 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/save-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { ImageDiffResult, SaveScreenshotRequest } from '../../../typings'; 2 | 3 | const saveScreenshot = jest.fn(); 4 | saveScreenshot.mockImplementation( 5 | (data: SaveScreenshotRequest): Promise => { 6 | return new Promise((resolve) => { 7 | resolve({ 8 | pass: true, 9 | screenshotId: data.id, 10 | }); 11 | }); 12 | }, 13 | ); 14 | export { saveScreenshot }; 15 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/test-screenshot-service.ts: -------------------------------------------------------------------------------- 1 | import { ImageDiffResult } from '../../../typings'; 2 | import { ScreenshotInfo } from '../../../../typings'; 3 | 4 | const testScreenshotService = jest.fn(); 5 | testScreenshotService.mockImplementation( 6 | (data: ScreenshotInfo): Promise => { 7 | return new Promise((resolve) => { 8 | resolve({ pass: true, screenshotId: data.screenshotId }); 9 | }); 10 | }, 11 | ); 12 | 13 | export { testScreenshotService }; 14 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/test-screenshots-service.ts: -------------------------------------------------------------------------------- 1 | import { ImageDiffResult } from '../../../typings'; 2 | 3 | const testScreenshots = jest.fn(); 4 | 5 | testScreenshots.mockImplementation( 6 | (): Promise => { 7 | return new Promise((resolve) => { 8 | resolve([{ pass: true, screenshotId: 'screenshot-id' }]); 9 | }); 10 | }, 11 | ); 12 | 13 | export { testScreenshots }; 14 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/test-story-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { ImageDiffResult } from '../../../typings'; 2 | 3 | const testStoryScreenshots = jest.fn(); 4 | 5 | testStoryScreenshots.mockImplementation( 6 | (): Promise => { 7 | return new Promise((resolve) => { 8 | resolve([{ pass: true, screenshotId: 'screenshot-id' }]); 9 | }); 10 | }, 11 | ); 12 | 13 | export { testStoryScreenshots }; 14 | -------------------------------------------------------------------------------- /src/api/server/services/__mocks__/update-screenshot-service.ts: -------------------------------------------------------------------------------- 1 | import { ImageDiffResult, UpdateScreenshot } from '../../../typings'; 2 | 3 | const updateScreenshotService = jest.fn(); 4 | 5 | updateScreenshotService.mockImplementation( 6 | (data: UpdateScreenshot): Promise => { 7 | return new Promise((resolve) => { 8 | resolve({ 9 | newScreenshot: data.base64, 10 | pass: true, 11 | screenshotId: data.screenshotId, 12 | }); 13 | }); 14 | }, 15 | ); 16 | 17 | export { updateScreenshotService }; 18 | -------------------------------------------------------------------------------- /src/api/server/services/__tests__/delete-story-screenshots.test.ts: -------------------------------------------------------------------------------- 1 | import { deleteStoryScreenshots } from '../delete-story-screenshots'; 2 | import { deleteScreenshot } from '../delete-screenshot'; 3 | 4 | jest.mock('../../utils/save-story-file'); 5 | jest.mock('../../utils/load-story-data'); 6 | jest.mock('../delete-screenshot'); 7 | 8 | describe('deleteStoryScreenshots', () => { 9 | it('should delete all', async () => { 10 | await deleteStoryScreenshots({ fileName: 'story.ts', storyId: 'story-id' }); 11 | expect(deleteScreenshot).toHaveBeenCalledTimes(2); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/api/server/services/__tests__/get-actions-schema.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { getActionsSchema } from '../get-actions-schema'; 3 | 4 | jest.mock('../../configs'); 5 | 6 | describe('getActionsSchema', () => { 7 | beforeEach(() => { 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | it('should return action schema', async () => { 12 | const schema = getActionsSchema(); 13 | expect(schema).toBeDefined(); 14 | }); 15 | 16 | it('should include custom schema', () => { 17 | const schema = getActionsSchema(); 18 | 19 | expect(schema['clickSelector']).toBeDefined(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/api/server/services/__tests__/get-schema-service.test.ts: -------------------------------------------------------------------------------- 1 | import { getSchemaService } from '../get-schema-service'; 2 | 3 | describe('getSchemaService', () => { 4 | beforeEach(() => { 5 | jest.clearAllMocks(); 6 | }); 7 | 8 | it('should generate PlaywrightScreenshotOptionSchema schema', () => { 9 | const schema = getSchemaService('browser-options'); 10 | expect(schema).toBeDefined(); 11 | }); 12 | 13 | it('should generate PlaywrightBrowserContextOptionSchema schema', () => { 14 | const schema = getSchemaService('screenshot-options'); 15 | expect(schema).toBeDefined(); 16 | }); 17 | 18 | it('should return nothing', () => { 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | const schema = getSchemaService('invalid' as any); 21 | expect(schema).not.toBeDefined(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/api/server/services/__tests__/get-theme-data.test.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@material-ui/core/styles'; 2 | import { getThemeData } from '../get-theme-data'; 3 | 4 | const mockTheme = createTheme({ 5 | palette: { 6 | primary: { 7 | main: '#00FF00', 8 | }, 9 | }, 10 | }); 11 | 12 | jest.mock('../../configs'); 13 | 14 | jest.mock('../get-theme-data', () => ({ 15 | getThemeData: () => mockTheme, 16 | })); 17 | 18 | describe('getThemeData', () => { 19 | beforeEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | it('should return theme data', async () => { 24 | const theme = getThemeData(); 25 | expect(theme).toBeDefined(); 26 | }); 27 | 28 | it('should include custom theme', () => { 29 | const theme = getThemeData(); 30 | 31 | expect(theme['palette']).toBeDefined(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/api/server/services/constants/common.ts: -------------------------------------------------------------------------------- 1 | export const FAVOURITE_ACTIONS_FILE_PATH = 2 | '.storybook/playwright-favourite-actions.json'; 3 | -------------------------------------------------------------------------------- /src/api/server/services/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | -------------------------------------------------------------------------------- /src/api/server/services/delete-favourite-action.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'jsonfile'; 2 | import { DeleteFavouriteAction } from '../../typings/delete-favourite-action'; 3 | import { FavouriteActions } from '../../typings/favourite-actions'; 4 | import { FAVOURITE_ACTIONS_FILE_PATH } from './constants'; 5 | import { getFavouriteActions } from './get-favourite-actions'; 6 | 7 | export const deleteFavouriteAction = async ( 8 | actionSet: DeleteFavouriteAction, 9 | ) => { 10 | const actionSets = await getFavouriteActions(); 11 | 12 | const favouriteActions: FavouriteActions = { 13 | actionSets: actionSets.filter((x) => x.id !== actionSet.actionSetId), 14 | }; 15 | 16 | writeFileSync(FAVOURITE_ACTIONS_FILE_PATH, favouriteActions, { 17 | EOL: '\r\n', 18 | spaces: 2, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/api/server/services/delete-story-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { StoryInfo } from '../../../typings'; 2 | import { getStoryScreenshotsData } from './get-story-screenshots-data'; 3 | import { deleteScreenshot } from './delete-screenshot'; 4 | 5 | export const deleteStoryScreenshots = async ( 6 | storyInfo: StoryInfo, 7 | ): Promise => { 8 | const screenshots = await getStoryScreenshotsData(storyInfo); 9 | 10 | for (let i = 0; i < screenshots.length; i++) { 11 | const screenshot = screenshots[i]; 12 | await deleteScreenshot({ 13 | fileName: storyInfo.fileName, 14 | screenshotId: screenshot.id, 15 | storyId: storyInfo.storyId, 16 | }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/api/server/services/get-action-set.ts: -------------------------------------------------------------------------------- 1 | import { loadStoryData, getStoryPlaywrightFileInfo } from '../utils'; 2 | import { ActionSet, StoryInfo } from '../../../typings'; 3 | import { getStoryData } from './utils'; 4 | import { nanoid } from 'nanoid'; 5 | 6 | export const getActionSet = async (data: StoryInfo): Promise => { 7 | const fileInfo = getStoryPlaywrightFileInfo(data.fileName); 8 | const storyData = await loadStoryData(fileInfo.path, data.storyId); 9 | 10 | const story = getStoryData(storyData, data.storyId); 11 | 12 | if (!story) return undefined; 13 | 14 | return story.actionSets 15 | ? story.actionSets.map((actionSet) => { 16 | actionSet.actions.forEach((action) => { 17 | action.id = nanoid(); 18 | }); 19 | return actionSet; 20 | }) 21 | : []; 22 | }; 23 | -------------------------------------------------------------------------------- /src/api/server/services/get-actions-schema.ts: -------------------------------------------------------------------------------- 1 | import { ActionSchemaList } from '../../../typings'; 2 | import { getConfigs } from '../configs'; 3 | import actionSchema from '../data/action-schema.json'; 4 | 5 | export const getActionsSchema = () => { 6 | let schema = actionSchema as ActionSchemaList; 7 | 8 | const customSchema = getConfigs().customActionSchema; 9 | 10 | if (customSchema) { 11 | schema = { ...schema, ...customSchema }; 12 | } 13 | return schema; 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/server/services/get-favourite-actions.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'jsonfile'; 2 | import { FavouriteActionSet } from '../../../typings'; 3 | import { FAVOURITE_ACTIONS_FILE_PATH } from './constants'; 4 | import * as fs from 'fs'; 5 | import { FavouriteActions } from '../../typings/favourite-actions'; 6 | 7 | export const getFavouriteActions = async (): Promise => { 8 | return new Promise((resolve, reject) => { 9 | if (!fs.existsSync(FAVOURITE_ACTIONS_FILE_PATH)) { 10 | resolve([]); 11 | return; 12 | } 13 | 14 | readFile(FAVOURITE_ACTIONS_FILE_PATH, (err, data?: FavouriteActions) => { 15 | if (err) { 16 | reject(err); 17 | } 18 | 19 | resolve(data.actionSets); 20 | }); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/api/server/services/get-schema-service.ts: -------------------------------------------------------------------------------- 1 | import screenshotOptionSchema from '../data/screenshot-option-schema.json'; 2 | import browserOptionSchema from '../data/browser-option-schema.json'; 3 | 4 | export type SchemaName = 'browser-options' | 'screenshot-options'; 5 | 6 | export const getSchemaService = (schemaName: SchemaName) => { 7 | if (schemaName === 'browser-options') { 8 | return browserOptionSchema; 9 | } 10 | if (schemaName === 'screenshot-options') { 11 | return screenshotOptionSchema; 12 | } 13 | return undefined; 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/server/services/get-story-screenshots-data.ts: -------------------------------------------------------------------------------- 1 | import { getStoryPlaywrightFileInfo, loadStoryData } from '../utils'; 2 | import { StoryInfo } from '../../../typings'; 3 | import { setStoryScreenshotOptions } from './utils/set-story-screenshot-options'; 4 | import { getStoryData } from './utils'; 5 | 6 | export const getStoryScreenshotsData = async (info: StoryInfo) => { 7 | const fileInfo = getStoryPlaywrightFileInfo(info.fileName); 8 | const storyData = await loadStoryData(fileInfo.path, info.storyId); 9 | 10 | const story = getStoryData(storyData, info.storyId); 11 | 12 | if (!story || !story.screenshots || !story.screenshots.length) 13 | return undefined; 14 | 15 | return story.screenshots.map((screenshot) => { 16 | setStoryScreenshotOptions(storyData, screenshot); 17 | return screenshot; 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/server/services/get-theme-data.ts: -------------------------------------------------------------------------------- 1 | import { getConfigs } from "../configs"; 2 | 3 | export const getThemeData = () => { 4 | return getConfigs().theme; 5 | } -------------------------------------------------------------------------------- /src/api/server/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-action-set'; 2 | export * from './get-actions-schema'; 3 | export * from './make-screenshot'; 4 | export * from './save-action-set'; 5 | export * from './save-screenshot'; 6 | -------------------------------------------------------------------------------- /src/api/server/services/utils/__mocks__/get-options-key.ts: -------------------------------------------------------------------------------- 1 | export const getOptionsKey = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/api/server/services/utils/__mocks__/set-story-screenshot-options.ts: -------------------------------------------------------------------------------- 1 | export const setStoryScreenshotOptions = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/api/server/services/utils/__tests__/get-story-data.test.ts: -------------------------------------------------------------------------------- 1 | import { getStoryData } from '../get-story-data'; 2 | 3 | describe('getStoryData', () => { 4 | it('should return story data', () => { 5 | const data = getStoryData( 6 | { stories: { 'story-id': { actionSets: [] } } }, 7 | 'story-id', 8 | ); 9 | expect(data).toStrictEqual({ actionSets: [] }); 10 | }); 11 | 12 | it('should not create empty story object', () => { 13 | const data = getStoryData({}, 'story-id'); 14 | expect(data).toStrictEqual(undefined); 15 | }); 16 | 17 | it('should create empty story object', () => { 18 | const data = getStoryData({}, 'story-id', true); 19 | expect(data).toStrictEqual({}); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/api/server/services/utils/__tests__/get-story-playwright-data-by-file-name.test.ts: -------------------------------------------------------------------------------- 1 | import { getStoryPlaywrightDataByFileName } from '../get-story-playwright-data-by-file-name'; 2 | 3 | describe('getStoryPlaywrightDataByFileName', () => { 4 | it('should be defined', () => { 5 | expect(getStoryPlaywrightDataByFileName).toBeDefined(); 6 | }); 7 | 8 | it('should call', () => { 9 | expect(getStoryPlaywrightDataByFileName('file-name')).toBeDefined(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/api/server/services/utils/delete-empty-story.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightData } from '../../../../typings'; 2 | 3 | import { getStoryData } from './get-story-data'; 4 | 5 | export const deleteEmptyStory = (data: PlaywrightData, storyId: string) => { 6 | const story = getStoryData(data, storyId); 7 | if (!story) return data; 8 | if (!Object.keys(story).length) { 9 | delete data.stories[storyId]; 10 | } 11 | return data; 12 | }; 13 | -------------------------------------------------------------------------------- /src/api/server/services/utils/get-options-key.ts: -------------------------------------------------------------------------------- 1 | import equal from 'fast-deep-equal'; 2 | import { StoryOptions, PlaywrightData } from '../../../../typings'; 3 | 4 | export const getOptionsKey = ( 5 | storyData: PlaywrightData, 6 | optionProp: keyof StoryOptions, 7 | // options?: BrowserContextOptions | ScreenshotOptions, 8 | options?: unknown, 9 | ) => { 10 | if (!options || !storyData[optionProp]) return undefined; 11 | 12 | const keys = Object.keys(storyData[optionProp]); 13 | 14 | for (let i = 0; i < keys.length; i++) { 15 | const key = keys[i]; 16 | const opt = storyData[optionProp][key]; 17 | if (equal(opt, options)) { 18 | return key; 19 | } 20 | } 21 | return undefined; 22 | }; 23 | -------------------------------------------------------------------------------- /src/api/server/services/utils/get-story-data.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightData } from '../../../../typings'; 2 | 3 | export const getStoryData = ( 4 | data: PlaywrightData, 5 | storyId: string, 6 | create = false, 7 | ) => { 8 | if (!create && (!data || !data.stories || !data.stories[storyId])) { 9 | return undefined; 10 | } 11 | 12 | if (!data.stories) data.stories = {}; 13 | if (!data.stories[storyId]) data.stories[storyId] = {}; 14 | 15 | return data.stories[storyId]; 16 | }; 17 | -------------------------------------------------------------------------------- /src/api/server/services/utils/get-story-playwright-data-by-file-name.ts: -------------------------------------------------------------------------------- 1 | import { getStoryPlaywrightFileInfo, loadStoryData } from '../../utils'; 2 | 3 | export const getStoryPlaywrightDataByFileName = async (fileName: string) => { 4 | const fileInfo = getStoryPlaywrightFileInfo(fileName); 5 | const storyData = await loadStoryData(fileInfo.path, '*'); 6 | return storyData; 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/server/services/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './delete-story-options'; 2 | export * from './get-screenshot-data'; 3 | export * from './get-story-data'; 4 | export * from './set-story-screenshot-options'; 5 | export * from './delete-empty-story'; 6 | export * from './find-screenshot-with-same-setting'; 7 | export * from './set-story-options'; 8 | export * from './should-take-screenshot'; 9 | export * from './get-story-playwright-data-by-file-name'; 10 | export * from './release-modifier-Key'; 11 | export * from './is-interactive-action'; 12 | -------------------------------------------------------------------------------- /src/api/server/services/utils/is-interactive-action.ts: -------------------------------------------------------------------------------- 1 | import { StoryAction } from '../../../../typings'; 2 | import { Page } from 'playwright'; 3 | 4 | const waitActions = [ 5 | 'waitForSelector', 6 | 'waitForTimeout', 7 | 'takeScreenshot', 8 | 'takeScreenshotAll', 9 | 'takeScreenshotOptions', 10 | ]; 11 | 12 | export const isInteractiveAction = (action: StoryAction) => { 13 | return !waitActions.includes(action.name); 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/server/services/utils/set-story-options.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightData, StoryOptions } from '../../../../typings'; 2 | 3 | import { nanoid } from 'nanoid'; 4 | import { getOptionsKey } from './get-options-key'; 5 | 6 | export const setStoryOptions = ( 7 | storyData: PlaywrightData, 8 | optionProp: keyof StoryOptions, 9 | // options?: BrowserContextOptions | ScreenshotOptions, 10 | options?: unknown, 11 | ) => { 12 | if (!options || !Object.keys(options).length) return undefined; 13 | 14 | if (!storyData[optionProp]) { 15 | storyData[optionProp] = {}; 16 | } 17 | 18 | const optionsKey = getOptionsKey(storyData, optionProp, options); 19 | 20 | if (optionsKey) { 21 | return optionsKey; 22 | } 23 | 24 | const id = nanoid(12); 25 | 26 | storyData[optionProp][id] = options; 27 | 28 | return id; 29 | }; 30 | -------------------------------------------------------------------------------- /src/api/server/services/utils/set-story-screenshot-options.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotData, PlaywrightData } from '../../../../typings'; 2 | 3 | export const setStoryScreenshotOptions = ( 4 | storyData: PlaywrightData, 5 | screenshot: ScreenshotData, 6 | ) => { 7 | if (storyData.browserOptions && screenshot.browserOptionsId) { 8 | screenshot.browserOptions = 9 | storyData.browserOptions[screenshot.browserOptionsId]; 10 | } 11 | if (storyData.screenshotOptions && screenshot.screenshotOptionsId) { 12 | screenshot.screenshotOptions = 13 | storyData.screenshotOptions[screenshot.screenshotOptionsId]; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/server/services/utils/should-take-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { StoryAction } from '../../../../typings'; 2 | import { Page } from 'playwright'; 3 | import { isInteractiveAction } from './is-interactive-action'; 4 | 5 | export const shouldTakeScreenshot = ( 6 | actions: StoryAction[], 7 | currentPosition: number, 8 | enabled?: boolean, 9 | ) => { 10 | // const action = actions[currentPosition]; 11 | 12 | // if (waitActions.includes(action.name)) return false; 13 | 14 | const nextAction = actions[currentPosition + 1]; 15 | 16 | return ( 17 | enabled && 18 | actions.length > 1 && 19 | actions.length > currentPosition + 1 && 20 | nextAction && 21 | isInteractiveAction(nextAction) 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/api/server/utils/__mocks__/execute-action.ts: -------------------------------------------------------------------------------- 1 | export const executeAction = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/api/server/utils/__mocks__/install-mouse-helper.ts: -------------------------------------------------------------------------------- 1 | export const installMouseHelper = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/api/server/utils/__mocks__/load-story-data.ts: -------------------------------------------------------------------------------- 1 | import { storyFileInfo } from '../../../../../__test_data__/story-file-info'; 2 | 3 | const loadStoryData = jest.fn(); 4 | 5 | loadStoryData.mockImplementation( 6 | (_filePAth: string, storyId: string, create = true) => { 7 | return new Promise((resolve) => { 8 | const data = storyFileInfo(); 9 | if (!data.stories[storyId] && storyId !== '*') { 10 | if (!create) { 11 | resolve(undefined); 12 | return; 13 | } 14 | data.stories[storyId] = {}; 15 | } 16 | resolve(data); 17 | }); 18 | }, 19 | ); 20 | 21 | export { loadStoryData }; 22 | -------------------------------------------------------------------------------- /src/api/server/utils/__mocks__/save-story-file.ts: -------------------------------------------------------------------------------- 1 | const saveStoryFile = jest.fn(); 2 | 3 | saveStoryFile.mockImplementation(() => { 4 | return new Promise((resolve) => { 5 | resolve(); 6 | }); 7 | }); 8 | 9 | export { saveStoryFile }; 10 | -------------------------------------------------------------------------------- /src/api/server/utils/__tests__/construct-screenshot-file-name.test.ts: -------------------------------------------------------------------------------- 1 | import { constructScreenshotFileName } from '../construct-screenshot-file-name'; 2 | 3 | describe('constructScreenshotFileName', () => { 4 | it('should be defined', () => { 5 | expect(constructScreenshotFileName).toBeDefined(); 6 | }); 7 | 8 | it('should construst', () => { 9 | expect( 10 | constructScreenshotFileName({ 11 | browser: 'chromium', 12 | screenshotTitle: 'screenshot-title', 13 | storyId: 'story-id', 14 | storyTitle: 'story-title', 15 | }), 16 | ).toBe('story-title-story-id-screenshot-title-chromium-snap.png'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/server/utils/__tests__/get-screenshot-paths.test.ts: -------------------------------------------------------------------------------- 1 | import { getScreenshotPaths } from '../get-screenshot-paths'; 2 | 3 | describe('getScreenshotPaths', () => { 4 | it('should be defined', () => { 5 | expect(getScreenshotPaths).toBeDefined(); 6 | }); 7 | 8 | it('should call function', () => { 9 | expect( 10 | getScreenshotPaths({ 11 | browserType: 'chromium', 12 | fileName: 'file-name', 13 | storyId: 'story-id', 14 | title: 'title', 15 | }), 16 | ).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/server/utils/__tests__/get-story-id-form-screenshot-file-name.test.ts: -------------------------------------------------------------------------------- 1 | import { getStoryIdFormScreenshotFileName } from '../get-story-id-form-screenshot-file-name'; 2 | 3 | describe('getStoryIdFormScreenshotFileName', () => { 4 | it('should be defined', () => { 5 | expect(getStoryIdFormScreenshotFileName).toBeDefined(); 6 | }); 7 | 8 | it('should return story id', () => { 9 | expect( 10 | getStoryIdFormScreenshotFileName({ 11 | browser: 'chromium', 12 | fileName: 'story-title-with-tippy-screenshot-title-chromium-snap.png', 13 | screenshotTitle: 'screenshot-title', 14 | storyTitle: 'story-title', 15 | }), 16 | ).toBe('with-tippy'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/api/server/utils/__tests__/get-story-playwright-file-info.test.ts: -------------------------------------------------------------------------------- 1 | import { getStoryPlaywrightFileInfo } from '../get-story-playwright-file-info'; 2 | 3 | describe('getStoryFileInfo', () => { 4 | it('should return file info', () => { 5 | const fileInfo = getStoryPlaywrightFileInfo('./stories/story.ts'); 6 | expect(fileInfo.name).toBe('story.playwright.json'); 7 | expect(fileInfo.path.endsWith('story.playwright.json')).toBeTruthy(); 8 | }); 9 | 10 | it('should handle playwright json file', () => { 11 | const fileInfo = getStoryPlaywrightFileInfo( 12 | './stories/story.playwright.json', 13 | ); 14 | expect(fileInfo.name).toBe('story.playwright.json'); 15 | expect(fileInfo.path.endsWith('story.playwright.json')).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/api/server/utils/construct-screenshot-file-name.ts: -------------------------------------------------------------------------------- 1 | import kebabCase from 'lodash/kebabCase'; 2 | 3 | interface Data { 4 | storyTitle: string; 5 | // named export function 6 | storyId: string; 7 | screenshotTitle: string; 8 | browser: string; 9 | } 10 | 11 | export const constructScreenshotFileName = (data: Data) => { 12 | const { screenshotTitle, storyId, storyTitle, browser } = data; 13 | 14 | return ( 15 | kebabCase( 16 | storyTitle + ' ' + storyId + ' ' + ' ' + screenshotTitle + ' ' + browser, 17 | ) + '-snap.png' 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/server/utils/get-story-playwright-data.ts: -------------------------------------------------------------------------------- 1 | import { loadStoryData } from './load-story-data'; 2 | import { PlaywrightStoryData } from '../../../typings'; 3 | 4 | export interface StoryPlaywrightData { 5 | data: PlaywrightStoryData; 6 | storyId: string; 7 | } 8 | 9 | export const getStoryPlaywrightData = async (fileName: string) => { 10 | const playWrightData = await loadStoryData(fileName, '*'); 11 | 12 | const stories = Object.keys(playWrightData.stories); 13 | 14 | const storyData: StoryPlaywrightData[] = stories.map((story) => { 15 | return { 16 | data: playWrightData.stories[story], 17 | storyId: story, 18 | }; 19 | }); 20 | 21 | return { 22 | playWrightData, 23 | storyData, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/api/server/utils/get-story-playwright-file-info.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export interface StoryPlaywrightFileInfo { 4 | name: string; 5 | path: string; 6 | dir: string; 7 | } 8 | 9 | export const getStoryPlaywrightFileInfo = (storyRelativeFilePath: string) => { 10 | const absolutePath = path.resolve(storyRelativeFilePath); 11 | const parsedFileName = path.parse(absolutePath); 12 | const name = 13 | parsedFileName.ext === '.json' 14 | ? `${parsedFileName.name}.json` 15 | : `${parsedFileName.name}.playwright.json`; 16 | return { 17 | dir: parsedFileName.dir, 18 | name: name, 19 | path: path.join(path.dirname(absolutePath), name), 20 | screenShotsDir: path.join(parsedFileName.dir, '__screenshots__'), 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/api/server/utils/get-version.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export const getVersion = () => { 4 | const packagePath = path.resolve(__dirname, '../../../../package.json'); 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const version = (require(packagePath).version as string) 8 | .split('.')[0] 9 | .toString(); 10 | 11 | return version; 12 | }; 13 | -------------------------------------------------------------------------------- /src/api/server/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './execute-action'; 2 | export * from './load-story-data'; 3 | export * from './get-story-playwright-file-info'; 4 | export * from './save-story-file'; 5 | export * from './get-story-playwright-data'; 6 | export * from './install-mouse-helper'; 7 | export * from './get-version'; 8 | export * from './construct-screenshot-file-name'; 9 | -------------------------------------------------------------------------------- /src/api/typings/delete-action-set.ts: -------------------------------------------------------------------------------- 1 | import { StoryInfo } from '../../typings'; 2 | 3 | export interface DeleteActionSetRequest extends StoryInfo { 4 | actionSetId: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/api/typings/delete-favourite-action.ts: -------------------------------------------------------------------------------- 1 | export interface DeleteFavouriteAction { 2 | actionSetId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/api/typings/favourite-actions.ts: -------------------------------------------------------------------------------- 1 | import { FavouriteActionSet } from '../../typings'; 2 | 3 | export interface FavouriteActions { 4 | actionSets: FavouriteActionSet[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/api/typings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './delete-action-set'; 2 | export * from './delete-favourite-action'; 3 | export * from './image-diff'; 4 | export * from './save-action-set'; 5 | export * from './schema-types'; 6 | export * from './screenshot-file-name'; 7 | export * from './screenshot-request-response'; 8 | export * from './test-screenshots'; 9 | -------------------------------------------------------------------------------- /src/api/typings/save-action-set.ts: -------------------------------------------------------------------------------- 1 | import { ActionSet, StoryInfo } from '../../typings'; 2 | 3 | export interface SaveActionSetRequest extends StoryInfo { 4 | actionSet: ActionSet; 5 | storyId: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/api/typings/screenshot-file-name.ts: -------------------------------------------------------------------------------- 1 | import { StoryData } from '../../typings'; 2 | 3 | export interface FixScreenshotFileName extends StoryData { 4 | previousNamedExport?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/api/typings/screenshot-request-response.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotSetting } from '../../typings'; 2 | 3 | import { ScreenshotData, StoryInfo, ScreenshotInfo } from '../../typings'; 4 | import { RequestData } from '../../typings/request'; 5 | 6 | export interface ScreenshotRequest extends ScreenshotSetting, RequestData { 7 | storyId: string; 8 | } 9 | 10 | export type GetScreenshotResponse = { 11 | base64: string; 12 | error: string; 13 | id: string; 14 | }; 15 | 16 | export interface SaveScreenshotRequest extends ScreenshotData, StoryInfo { 17 | base64?: string; 18 | id: string; 19 | updateScreenshot?: ScreenshotData; 20 | } 21 | 22 | export interface UpdateScreenshot extends ScreenshotInfo { 23 | base64?: string; 24 | } 25 | 26 | export interface ChangeScreenshotIndex extends StoryInfo { 27 | oldIndex: number; 28 | newIndex: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/api/typings/test-screenshots.ts: -------------------------------------------------------------------------------- 1 | import { RequestData } from '../../typings/request'; 2 | import { StoryInfo, ScreenshotTestTargetType } from '../../typings'; 3 | 4 | export interface TestScreenShots extends RequestData, StoryInfo { 5 | requestType: ScreenshotTestTargetType; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/action-set-panel/__tests__/ActionSetToolbar.test.tsx: -------------------------------------------------------------------------------- 1 | import { ActionToolbar } from '../ActionSetToolbar'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | 5 | describe('ActionToolbar', () => { 6 | it('should render', () => { 7 | const wrapper = shallow( 8 | , 13 | ); 14 | expect(wrapper.exists()).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/action-set-panel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionSetMain'; 2 | -------------------------------------------------------------------------------- /src/components/action-set-panel/utils/__tests__/__snapshots__/get-menu.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`getMenu should return valid 1`] = ` 4 | Array [ 5 | Object { 6 | "label": "click", 7 | "name": "click", 8 | }, 9 | Object { 10 | "label": "mouse.click", 11 | "name": "mouse.click", 12 | }, 13 | Object { 14 | "label": "mouse.dblclick", 15 | "name": "mouse.dblclick", 16 | }, 17 | ] 18 | `; 19 | -------------------------------------------------------------------------------- /src/components/action-set-panel/utils/filter-favourite-actions.ts: -------------------------------------------------------------------------------- 1 | import { FavouriteActionSet } from '../../../typings/story-action'; 2 | 3 | export const filterFavouriteActions = ( 4 | actions: FavouriteActionSet[], 5 | storyId: string, 6 | ) => { 7 | return actions.reduce((arr, item) => { 8 | if (item.visibleTo === '*' || new RegExp(item.visibleTo).test(storyId)) { 9 | arr.push(item); 10 | } 11 | return arr; 12 | }, []); 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/action-set-panel/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-menu'; 2 | export * from './filter-favourite-actions'; 3 | -------------------------------------------------------------------------------- /src/components/actions/__tests__/__snapshots__/ActionList.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ActionList should show error if no actions provided 1`] = ` 4 |
7 |
8 | Click the 9 | '+' 10 | button to create an action. 11 |
12 |
13 | `; 14 | -------------------------------------------------------------------------------- /src/components/actions/utils/get-action-option-value.ts: -------------------------------------------------------------------------------- 1 | import { StoryAction, ActionSchema } from '../../../typings'; 2 | import * as immutableObject from 'object-path-immutable'; 3 | 4 | export const getActionOptionValue = ( 5 | action: StoryAction, 6 | optionPath: string, 7 | schema?: ActionSchema, 8 | ): undefined | unknown => { 9 | if (action && action.args) { 10 | return immutableObject.get(action.args, optionPath); 11 | } 12 | return schema ? schema.default : undefined; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/actions/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-action-option-value'; 2 | -------------------------------------------------------------------------------- /src/components/common/BrowserIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { BrowserTypes } from '../../typings'; 3 | import { Firefox, Browser, Chrome, Webkit } from '../../icons'; 4 | import { SvgIconProps } from '@material-ui/core'; 5 | 6 | export interface BrowserIconProps extends SvgIconProps { 7 | browserType: BrowserTypes; 8 | } 9 | 10 | const BrowserIcon: React.FC = memo((props) => { 11 | const { browserType, ...rest } = props; 12 | 13 | switch (browserType) { 14 | case 'chromium': 15 | return ; 16 | case 'firefox': 17 | return ; 18 | case 'webkit': 19 | return ; 20 | default: 21 | return ; 22 | } 23 | }); 24 | 25 | BrowserIcon.displayName = 'BrowserIcon'; 26 | 27 | export { BrowserIcon }; 28 | -------------------------------------------------------------------------------- /src/components/common/CheckBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconButton } from '@material-ui/core'; 3 | import CheckBoxUnchecked from '@material-ui/icons/CheckBoxOutlineBlank'; 4 | import CheckBoxChecked from '@material-ui/icons/CheckBox'; 5 | 6 | export interface CheckBoxProps { 7 | onClick: () => void; 8 | checked: boolean; 9 | } 10 | 11 | const CheckBox: React.FC = (props) => { 12 | const { onClick, checked } = props; 13 | 14 | return ( 15 | 21 | {checked ? : } 22 | 23 | ); 24 | }; 25 | 26 | CheckBox.displayName = 'CheckBox'; 27 | 28 | export { CheckBox }; 29 | -------------------------------------------------------------------------------- /src/components/common/DragHandle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SortableHandle } from 'react-sortable-hoc'; 3 | import DragIndicatorSharp from '@material-ui/icons/DragIndicatorSharp'; 4 | 5 | export const DragHandle = SortableHandle(() => ( 6 | 9 | )); 10 | -------------------------------------------------------------------------------- /src/components/common/ErrorPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { makeStyles, Backdrop } from '@material-ui/core'; 3 | 4 | const useStyles = makeStyles( 5 | () => { 6 | return { 7 | backdrop: { 8 | position: 'absolute', 9 | }, 10 | root: { 11 | color: 'red', 12 | }, 13 | }; 14 | }, 15 | { name: 'ErrorPanel' }, 16 | ); 17 | 18 | export interface ErrorPanelProps { 19 | message: string; 20 | } 21 | 22 | const ErrorPanel: React.FC = memo((props) => { 23 | const { message } = props; 24 | 25 | const classes = useStyles(); 26 | 27 | return ( 28 | 29 |
{message}
{' '} 30 |
31 | ); 32 | }); 33 | 34 | ErrorPanel.displayName = 'ErrorPanel'; 35 | 36 | export { ErrorPanel }; 37 | -------------------------------------------------------------------------------- /src/components/common/ImageDiffPreviewDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ImageDiffPreview, ImageDiffPreviewProps } from './ImageDiffPreview'; 3 | import { Dialog, DialogProps } from './Dialog'; 4 | 5 | export interface ImageDiffPreviewDialogProps 6 | extends DialogProps, 7 | ImageDiffPreviewProps {} 8 | 9 | const ImageDiffPreviewDialog: React.FC = ( 10 | props, 11 | ) => { 12 | const { imageDiffResult, activeTab, ...rest } = props; 13 | 14 | return ( 15 | 16 | 20 | 21 | ); 22 | }; 23 | 24 | ImageDiffPreviewDialog.displayName = 'ImageDiffPreviewDialog'; 25 | 26 | export { ImageDiffPreviewDialog }; 27 | -------------------------------------------------------------------------------- /src/components/common/__tests__/ActionPanel.test.tsx: -------------------------------------------------------------------------------- 1 | import { ActionPanel } from '../ActionPanel'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | describe('ActionPanel', () => { 5 | it('should render', () => { 6 | const wrapper = shallow(); 7 | expect(wrapper.exists()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/common/__tests__/ActionPopover.test.tsx: -------------------------------------------------------------------------------- 1 | import { ActionPopover } from '../ActionPopover'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | 5 | describe('ActionPopover', () => { 6 | const OnCloseMock = jest.fn(); 7 | afterEach(() => { 8 | OnCloseMock.mockClear(); 9 | }); 10 | 11 | it('should render', () => { 12 | const wrapper = shallow(); 13 | expect(wrapper.exists()).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/common/__tests__/CheckBox.test.tsx: -------------------------------------------------------------------------------- 1 | import { CheckBox } from '../CheckBox'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | import CheckBoxUnchecked from '@material-ui/icons/CheckBoxOutlineBlank'; 5 | import CheckBoxChecked from '@material-ui/icons/CheckBox'; 6 | 7 | describe('CheckBox', () => { 8 | it('should render', () => { 9 | const wrapper = shallow(); 10 | expect(wrapper.exists()).toBeTruthy(); 11 | expect(wrapper.find(CheckBoxUnchecked)).toHaveLength(1); 12 | }); 13 | it('should check', () => { 14 | const wrapper = shallow(); 15 | expect(wrapper.find(CheckBoxChecked)).toHaveLength(1); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/common/__tests__/CommonProvider.test.tsx: -------------------------------------------------------------------------------- 1 | import { useEffectCleanup } from '../../../../__manual_mocks__/react-useEffect'; 2 | import { CommonProvider } from '../CommonProvider'; 3 | import { shallow } from 'enzyme'; 4 | import React from 'react'; 5 | 6 | describe('CommonProvider', () => { 7 | it('should render', () => { 8 | const wrapper = shallow( 9 | 10 |
test
11 |
, 12 | ); 13 | expect(wrapper.exists()).toBeTruthy(); 14 | }); 15 | 16 | it('should remove root element when unmounted', () => { 17 | const spy = jest.spyOn(document.body, 'removeChild'); 18 | shallow( 19 | 20 |
test
21 |
, 22 | ); 23 | useEffectCleanup(); 24 | expect(spy).toHaveBeenCalledTimes(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/common/__tests__/Dialog.test.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog } from '../Dialog'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | describe('Dialog', () => { 5 | it('should render', () => { 6 | const wrapper = shallow(); 7 | expect(wrapper.exists()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/common/__tests__/DragHandle.test.tsx: -------------------------------------------------------------------------------- 1 | import { DragHandle } from '../DragHandle'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | 5 | describe('DragHandle', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(, { disableLifecycleMethods: true }) 8 | .first() 9 | .shallow(); 10 | expect(wrapper.exists()).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/common/__tests__/ErrorPanel.test.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorPanel } from '../ErrorPanel'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | 5 | describe('ErrorPanel', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(); 8 | expect(wrapper.exists()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/common/__tests__/FixScreenshotFileDialog.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { FixScreenshotFileDialog } from '../FixScreenshotFileDialog'; 4 | 5 | describe('FixScreenshotFileDialog', () => { 6 | it('should be defined', () => { 7 | const wrapper = shallow(); 8 | 9 | expect(wrapper).toBeDefined(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/common/__tests__/ImageDiffPreviewDialog.test.tsx: -------------------------------------------------------------------------------- 1 | import { ImageDiffPreviewDialog } from '../ImageDiffPreviewDialog'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | describe('ImageDiffPreviewDialog', () => { 5 | it('should render', () => { 6 | const wrapper = shallow( 7 | , 8 | ); 9 | expect(wrapper).toHaveLength(1); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/common/__tests__/ListWrapper.test.tsx: -------------------------------------------------------------------------------- 1 | import { ListWrapper } from '../ListWrapper'; 2 | import { shallow } from 'enzyme'; 3 | import React, { createElement } from 'react'; 4 | 5 | jest.mock('react', () => ({ 6 | ...jest.requireActual('react'), 7 | useEffect: (cb: () => void) => { 8 | cb(); 9 | }, 10 | useRef: () => ({ current: { querySelector: () => createElement('div') } }), 11 | })); 12 | 13 | describe('ListWrapper', () => { 14 | it('should render', () => { 15 | const wrapper = shallow(); 16 | expect(wrapper.exists()).toBeTruthy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/common/__tests__/Loader.test.tsx: -------------------------------------------------------------------------------- 1 | import { Loader } from '../Loader'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | describe('Loader', () => { 5 | it('should render', () => { 6 | const wrapper = shallow(); 7 | expect(wrapper.exists()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/common/__tests__/Toolbar.test.tsx: -------------------------------------------------------------------------------- 1 | import { Toolbar } from '../Toolbar'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | describe('Toolbar', () => { 5 | it('should render', () => { 6 | const wrapper = shallow(); 7 | expect(wrapper.exists()).toBeTruthy(); 8 | }); 9 | 10 | it('should have border', () => { 11 | const wrapper = shallow(); 12 | expect(wrapper.find('div').first().hasClass('border-top')).toBeTruthy(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './preview'; 3 | export * from './screenshot-preview'; 4 | export * from './tool-bar'; 5 | export * from './screenshot-panel'; 6 | -------------------------------------------------------------------------------- /src/components/panel/ActionPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { ActionSetMain } from '../action-set-panel'; 3 | import { CommonProvider } from '../common'; 4 | import { ActionProvider } from '../../store'; 5 | 6 | const ActionPanel: React.FC = memo(() => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }); 15 | 16 | ActionPanel.displayName = 'ActionPanel'; 17 | 18 | export { ActionPanel }; 19 | -------------------------------------------------------------------------------- /src/components/panel/ScreenshotPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useStorybookState } from '@storybook/api'; 3 | import { SCREENSHOT_PANEL_ID } from '../../constants'; 4 | import { ScreenshotMain } from '../screenshot-panel/ScreenshotMain'; 5 | 6 | const ScreenshotPanel: React.FC = () => { 7 | const state = useStorybookState(); 8 | 9 | return ( 10 | 11 | ); 12 | }; 13 | 14 | ScreenshotPanel.displayName = 'ScreenshotPanel'; 15 | 16 | export { ScreenshotPanel }; 17 | -------------------------------------------------------------------------------- /src/components/panel/__tests__/ActionPanel.test.tsx: -------------------------------------------------------------------------------- 1 | import { ActionPanel } from '../ActionPanel'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | describe('ActionPanel', () => { 5 | it('should render', () => { 6 | const wrapper = shallow(); 7 | expect(wrapper.exists()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/panel/__tests__/ScreenshotPanel.test.tsx: -------------------------------------------------------------------------------- 1 | import { ScreenshotPanel } from '../ScreenshotPanel'; 2 | import { ScreenshotMain } from '../../screenshot-panel/ScreenshotMain'; 3 | import { shallow } from 'enzyme'; 4 | import React from 'react'; 5 | import { useStorybookState } from '@storybook/api'; 6 | import { SCREENSHOT_PANEL_ID } from '../../../constants'; 7 | 8 | describe('ScreenshotPanel', () => { 9 | beforeEach(() => { 10 | jest.clearAllMocks(); 11 | }); 12 | 13 | it('should render', () => { 14 | (useStorybookState as jest.Mock).mockImplementation(() => ({ 15 | selectedPanel: SCREENSHOT_PANEL_ID, 16 | })); 17 | const wrapper = shallow(); 18 | 19 | expect(wrapper.find(ScreenshotMain)).toHaveLength(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/panel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionPanel'; 2 | export * from './ScreenshotPanel'; 3 | -------------------------------------------------------------------------------- /src/components/preview/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Preview'; 2 | -------------------------------------------------------------------------------- /src/components/preview/utils/__mocks__/is-horizontal.ts: -------------------------------------------------------------------------------- 1 | export const isHorizontal = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/components/preview/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './is-horizontal'; 2 | -------------------------------------------------------------------------------- /src/components/preview/utils/is-horizontal.ts: -------------------------------------------------------------------------------- 1 | import { AddonState } from '../../../typings'; 2 | 3 | export const isHorizontalPanel = ( 4 | addonState: AddonState, 5 | storybookPanelPosition: string, 6 | ) => { 7 | if (addonState && addonState.placement && addonState.placement !== 'auto') { 8 | if (addonState.placement === 'bottom') return true; 9 | return false; 10 | } 11 | if (storybookPanelPosition === 'bottom') return false; 12 | return true; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/schema/__tests__/SchemaRenderer.test.tsx: -------------------------------------------------------------------------------- 1 | import { SchemaRenderer } from '../SchemaRenderer'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | 5 | describe('SchemaRenderer', () => { 6 | const onChangeMock = jest.fn(); 7 | 8 | const getVal = () => { 9 | return null; 10 | }; 11 | 12 | beforeEach(() => { 13 | jest.clearAllMocks(); 14 | }); 15 | 16 | it('should render', () => { 17 | const wrapper = shallow( 18 | , 23 | ); 24 | 25 | expect(wrapper.exists()).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SchemaRenderer'; 2 | -------------------------------------------------------------------------------- /src/components/screenshot-panel/ScreenshotListItemWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ListItemWrapperProps, ListItemWrapper } from '../common'; 3 | 4 | const ScreenshotListItemWrapper: React.FC = ({ 5 | children, 6 | ...rest 7 | }) => { 8 | return ( 9 | 16 | ); 17 | }; 18 | 19 | ScreenshotListItemWrapper.displayName = 'ScreenshotListItemWrapper'; 20 | 21 | export { ScreenshotListItemWrapper }; 22 | -------------------------------------------------------------------------------- /src/components/screenshot-panel/__tests__/ScreenshotListItemWrapper.test.tsx: -------------------------------------------------------------------------------- 1 | import { ScreenshotListItemWrapper } from '../ScreenshotListItemWrapper'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | describe('ScreenshotListItemWrapper', () => { 5 | it('should render', () => { 6 | const wrapper = shallow( 7 | , 8 | ); 9 | expect(wrapper.exists()).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/screenshot-panel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ScreenshotMain'; 2 | -------------------------------------------------------------------------------- /src/components/screenshot-preview/__tests__/PreviewDialog.test.tsx: -------------------------------------------------------------------------------- 1 | import { PreviewDialog } from '../PreviewDialog'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | describe('PreviewDialog', () => { 5 | it('should render', () => { 6 | const wrapper = shallow(); 7 | expect(wrapper.exists()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/screenshot-preview/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PreviewDialog'; 2 | export * from './ScreenshotView'; 3 | export * from './ScreenshotListView'; 4 | -------------------------------------------------------------------------------- /src/components/screenshot-preview/utils/__tests__/get-border-color.test.ts: -------------------------------------------------------------------------------- 1 | import { getBorderColor } from '../get-border-color'; 2 | 3 | describe('getBorderColor', () => { 4 | it('should return lighter', () => { 5 | expect(getBorderColor('dark', '#000', 0.5)).toBe('rgb(127, 127, 127)'); 6 | }); 7 | it('should return darker', () => { 8 | expect(getBorderColor('light', '#fff', 0.5)).toBe('rgb(127, 127, 127)'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/screenshot-preview/utils/get-border-color.ts: -------------------------------------------------------------------------------- 1 | import { lighten, darken } from '@material-ui/core/styles'; 2 | 3 | export const getBorderColor = ( 4 | type: 'dark' | 'light', 5 | color: string, 6 | coefficient = 0.1, 7 | ) => { 8 | return type === 'dark' 9 | ? lighten(color, coefficient) 10 | : darken(color, coefficient); 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/screenshot-preview/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-border-color'; 2 | -------------------------------------------------------------------------------- /src/components/tool-bar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Tool'; 2 | -------------------------------------------------------------------------------- /src/constants/add-on.ts: -------------------------------------------------------------------------------- 1 | export const ADDON_ID = 'playwright-addon'; 2 | export const ACTIONS_PANEL_ID = `${ADDON_ID}/action-panel`; 3 | export const SCREENSHOT_PANEL_ID = `${ADDON_ID}/screenshot-panel`; 4 | export const TOOL_ID = `${ADDON_ID}/tool`; 5 | export const PREVIEW_ID = `${ADDON_ID}/preview-panel`; 6 | export const ADDON_STORAGE_KEY = `${ADDON_ID}/storage-key`; 7 | 8 | export const EVENTS = { 9 | SELECTOR: `${ADDON_ID}/selector`, 10 | }; 11 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-on'; 2 | export * from './messages'; 3 | -------------------------------------------------------------------------------- /src/constants/messages.ts: -------------------------------------------------------------------------------- 1 | export const TEMP_ACTION_SET = 2 | ' (This action loaded from screenshot actions, its temporary and will be removed)'; 3 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-action-editor.ts: -------------------------------------------------------------------------------- 1 | export const useActionEditor = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-action-schema-loader.ts: -------------------------------------------------------------------------------- 1 | export const useActionSchemaLoader = jest.fn(); 2 | 3 | useActionSchemaLoader.mockImplementation(() => ({ 4 | loading: false, 5 | })); 6 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-addon-state.ts: -------------------------------------------------------------------------------- 1 | export const useAddonState = jest.fn().mockImplementation(() => { 2 | return { 3 | addonState: jest.fn(), 4 | setAddonState: jest.fn(), 5 | }; 6 | }); 7 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-anchor-el.ts: -------------------------------------------------------------------------------- 1 | export const useAnchorEl = jest.fn(); 2 | 3 | useAnchorEl.mockImplementation(() => ({ 4 | anchorEl: document.createElement('div'), 5 | anchorElRef: {}, 6 | clearAnchorEl: jest.fn(), 7 | setAnchorEl: jest.fn(), 8 | })); 9 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-async-api-call.tsx: -------------------------------------------------------------------------------- 1 | const useAsyncApiCall = jest.fn(); 2 | 3 | useAsyncApiCall.mockImplementation(() => { 4 | return { 5 | ErrorSnackbar: jest.fn(), 6 | clearError: jest.fn(), 7 | clearResult: jest.fn(), 8 | error: undefined, 9 | inProgress: false, 10 | makeCall: jest.fn(), 11 | result: undefined, 12 | }; 13 | }); 14 | 15 | export { useAsyncApiCall }; 16 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-browser-options.ts: -------------------------------------------------------------------------------- 1 | export const useBrowserOptions = jest.fn().mockImplementation(() => ({ 2 | browserOptions: { all: {} }, 3 | getBrowserOptions: jest.fn(), 4 | setBrowserDeviceOptions: jest.fn(), 5 | setBrowserOptions: jest.fn(), 6 | })); 7 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-current-actions.ts: -------------------------------------------------------------------------------- 1 | export const useCurrentActions = jest.fn().mockReturnValue([]); 2 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-current-story-action-sets.ts: -------------------------------------------------------------------------------- 1 | export const useCurrentStoryActionSets = jest.fn().mockImplementation(() => ({ 2 | currentActionSets: [], 3 | state: {}, 4 | storyActionSets: [], 5 | })); 6 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-current-story-data.ts: -------------------------------------------------------------------------------- 1 | import { getStoryData } from '../../../__test_data__/story-data'; 2 | 3 | const useCurrentStoryData = jest.fn(); 4 | 5 | useCurrentStoryData.mockImplementation(() => getStoryData()); 6 | 7 | export { useCurrentStoryData }; 8 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-custom-theme.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@material-ui/core/styles'; 2 | import { mocked } from 'ts-jest/utils'; 3 | 4 | export const useCustomTheme = jest.fn(); 5 | 6 | const theme = createTheme({ 7 | palette: { 8 | primary: { 9 | main: '#ffffff', 10 | }, 11 | }, 12 | }); 13 | 14 | mocked(useCustomTheme).mockImplementation(() => ({ 15 | theme, 16 | })); 17 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-delete-screenshot.ts: -------------------------------------------------------------------------------- 1 | const useDeleteScreenshot = jest.fn(); 2 | 3 | useDeleteScreenshot.mockImplementation(() => { 4 | return { 5 | ErrorSnackbar: () => undefined, 6 | clearError: () => undefined, 7 | deleteScreenshot: () => undefined, 8 | inProgress: false, 9 | }; 10 | }); 11 | 12 | export { useDeleteScreenshot }; 13 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-drag-start.ts: -------------------------------------------------------------------------------- 1 | const useDragStart = jest.fn(); 2 | useDragStart.mockImplementation(() => { 3 | return { dragStart: false, setDragStart: jest.fn() }; 4 | }); 5 | 6 | export { useDragStart }; 7 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-edit-screenshot.ts: -------------------------------------------------------------------------------- 1 | // import { useEditScreenshot } from '../use-edit-screenshot'; 2 | 3 | const useEditScreenshot = jest.fn(); 4 | 5 | useEditScreenshot.mockImplementation(() => { 6 | return { 7 | clearScreenshotEdit: jest.fn(), 8 | editScreenshot: jest.fn(), 9 | editScreenshotState: undefined, 10 | isEditing: jest.fn(), 11 | loadSetting: jest.fn(), 12 | }; 13 | }); 14 | 15 | export { useEditScreenshot }; 16 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-editor-action.ts: -------------------------------------------------------------------------------- 1 | export const useEditorAction = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-global-action-dispatch.ts: -------------------------------------------------------------------------------- 1 | export const useGlobalActionDispatch = jest.fn(); 2 | 3 | useGlobalActionDispatch.mockImplementation((callBack) => callBack()); 4 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-global-dispatch.ts: -------------------------------------------------------------------------------- 1 | export const useGlobalDispatch = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-global-imageDiff-results.ts: -------------------------------------------------------------------------------- 1 | import { UseGlobalImageDiffResults } from '../use-global-imageDiff-results'; 2 | 3 | const useGlobalImageDiffResults = jest.fn(); 4 | 5 | useGlobalImageDiffResults.mockImplementation( 6 | (): UseGlobalImageDiffResults => { 7 | return { 8 | imageDiffResult: [], 9 | setImageDiffResult: () => undefined, 10 | }; 11 | }, 12 | ); 13 | 14 | export { useGlobalImageDiffResults }; 15 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-global-screenshot-dispatch.ts: -------------------------------------------------------------------------------- 1 | const useGlobalScreenshotDispatch = jest.fn(); 2 | 3 | export { useGlobalScreenshotDispatch }; 4 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-global-state1.ts: -------------------------------------------------------------------------------- 1 | // import{ 2 | 3 | // useState, 4 | // } from 'react'; 5 | 6 | // export const makeGlobalStateId = (id: string) => { 7 | // return `__playwright_${id}`; 8 | // }; 9 | 10 | // export const useGlobalState = ( 11 | // ): [T, Dispatch>] => { 12 | // const [globalState, setState] = useState(defaultState); 13 | 14 | // return [globalState, setGlobalState as Dispatch>]; 15 | // }; 16 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-imagediff-screenshots.ts: -------------------------------------------------------------------------------- 1 | const useImageDiffScreenshots = jest.fn(); 2 | useImageDiffScreenshots.mockImplementation(() => ({ 3 | loading: false, 4 | })); 5 | export { useImageDiffScreenshots }; 6 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-load-screenshot-settings.ts: -------------------------------------------------------------------------------- 1 | const useLoadScreenshotSettings = jest.fn(); 2 | 3 | useLoadScreenshotSettings.mockImplementation(() => ({ 4 | loadSetting: jest.fn(), 5 | })); 6 | 7 | export { useLoadScreenshotSettings }; 8 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-save-screenshot.ts: -------------------------------------------------------------------------------- 1 | export const useSaveScreenshot = jest.fn().mockImplementation(() => ({ 2 | ErrorSnackbar: () => null, 3 | getUpdatingScreenshotTitle: jest.fn(), 4 | inProgress: false, 5 | onSuccessClose: jest.fn(), 6 | result: undefined, 7 | saveScreenShot: jest.fn(), 8 | })); 9 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-screenshot-imageDiff-results.ts: -------------------------------------------------------------------------------- 1 | const useScreenshotImageDiffResults = jest.fn(); 2 | 3 | useScreenshotImageDiffResults.mockImplementation(() => { 4 | return { 5 | clearImageDiffError: jest.fn(), 6 | imageDiffTestInProgress: false, 7 | storyImageDiffError: undefined, 8 | testStoryScreenShots: jest.fn(), 9 | }; 10 | }); 11 | 12 | export { useScreenshotImageDiffResults }; 13 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-screenshot-imageDiff.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | const useScreenshotImageDiff = jest.fn(); 3 | 4 | useScreenshotImageDiff.mockImplementation(() => ({ 5 | TestScreenshotErrorSnackbar: () => createElement('div'), 6 | inProgress: false, 7 | testScreenshot: () => undefined, 8 | testScreenshotError: undefined, 9 | })); 10 | 11 | export { useScreenshotImageDiff }; 12 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-screenshot-index-change.ts: -------------------------------------------------------------------------------- 1 | const useScreenshotIndexChange = jest.fn(); 2 | 3 | useScreenshotIndexChange.mockImplementation(() => ({ 4 | ChangeIndexErrorSnackbar: () => undefined, 5 | ChangeIndexInProgress: false, 6 | ChangeIndexSuccessSnackbar: () => undefined, 7 | changeIndex: jest.fn(), 8 | })); 9 | 10 | export { useScreenshotIndexChange }; 11 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-screenshot-options.ts: -------------------------------------------------------------------------------- 1 | export const useScreenshotOptions = jest.fn(); 2 | 3 | useScreenshotOptions.mockImplementation(() => ({ 4 | screenshotOptions: undefined, 5 | setScreenshotOptions: () => undefined, 6 | })); 7 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-screenshot-update-state.ts: -------------------------------------------------------------------------------- 1 | import { mocked } from 'ts-jest/utils'; 2 | 3 | export const useScreenshotUpdateState = jest.fn(); 4 | 5 | mocked(useScreenshotUpdateState).mockImplementation(() => ({ 6 | handleClose: jest.fn(), 7 | handleLoadingDone: jest.fn(), 8 | runDiffTest: jest.fn(), 9 | updateInf: {}, 10 | })); 11 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-screenshot-update.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | const useScreenshotUpdate = jest.fn(); 4 | 5 | useScreenshotUpdate.mockImplementation(() => ({ 6 | UpdateScreenshotErrorSnackbar: createElement('div'), 7 | UpdateScreenshotSuccessSnackbar: createElement('div'), 8 | updateScreenshot: () => undefined, 9 | updateScreenshotClearResult: () => undefined, 10 | updateScreenshotInProgress: false, 11 | })); 12 | 13 | export { useScreenshotUpdate }; 14 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-screenshot.ts: -------------------------------------------------------------------------------- 1 | export const useScreenshot = jest.fn().mockImplementation(() => ({ 2 | getSnapshot: jest.fn(), 3 | loading: false, 4 | screenshot: undefined, 5 | })); 6 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-selector-manager.ts: -------------------------------------------------------------------------------- 1 | export const useSelectorManager = jest.fn().mockImplementation(() => ({ 2 | selectorManager: {}, 3 | setSelectorData: jest.fn(), 4 | startSelector: jest.fn(), 5 | stopSelector: jest.fn(), 6 | })); 7 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-snackbar.tsx: -------------------------------------------------------------------------------- 1 | export const useSnackbar = jest.fn(); 2 | -------------------------------------------------------------------------------- /src/hooks/__mocks__/use-story-action-sets-loader.ts: -------------------------------------------------------------------------------- 1 | export const useStoryActionSetsLoader = jest.fn().mockImplementation(() => ({ 2 | loading: false, 3 | retry: jest.fn(), 4 | })); 5 | -------------------------------------------------------------------------------- /src/hooks/__tests__/use-action-schema-loader.test.ts: -------------------------------------------------------------------------------- 1 | import { useActionSchemaLoader } from '../use-action-schema-loader'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | import fetch from 'jest-fetch-mock'; 4 | import { getActionSchemaData } from '../../../__test_data__'; 5 | import * as context from '../../store/actions/ActionContext'; 6 | 7 | describe('useActionSchemaLoader', () => { 8 | it('should test useActionSchemaLoader', async () => { 9 | fetch.mockResponseOnce(JSON.stringify(getActionSchemaData())); 10 | const { waitForNextUpdate, result } = renderHook( 11 | () => useActionSchemaLoader(), 12 | { 13 | wrapper: context.ActionProvider, 14 | }, 15 | ); 16 | 17 | await waitForNextUpdate(); 18 | 19 | expect(result.current.loaded).toBe(true); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/hooks/__tests__/use-active-browser.test.ts: -------------------------------------------------------------------------------- 1 | import { useActiveBrowsers } from '../use-active-browser'; 2 | import { renderHook, act } from '@testing-library/react-hooks'; 3 | 4 | describe('useActiveBrowsers', () => { 5 | it('should not be disabled', () => { 6 | const { result } = renderHook(() => useActiveBrowsers('main')); 7 | 8 | expect(result.current.isDisabled('chromium')).toBe(false); 9 | }); 10 | 11 | it('should be disable', () => { 12 | const { result } = renderHook(() => useActiveBrowsers('dialog')); 13 | 14 | act(() => { 15 | result.current.toggleBrowser('chromium'); 16 | }); 17 | 18 | expect(result.current.isDisabled('chromium')).toBe(true); 19 | 20 | act(() => { 21 | result.current.toggleBrowser('chromium'); 22 | }); 23 | 24 | expect(result.current.isDisabled('chromium')).toBe(false); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/hooks/__tests__/use-addon-state.test.ts: -------------------------------------------------------------------------------- 1 | import { useAddonState } from '../use-addon-state'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | 4 | describe('useAddonState', () => { 5 | it('should be object', () => { 6 | const { result } = renderHook(() => useAddonState()); 7 | 8 | expect(result.current.addonState).toStrictEqual({}); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/hooks/__tests__/use-drag-start.test.ts: -------------------------------------------------------------------------------- 1 | import { useDragStart } from '../use-drag-start'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | 4 | describe('useDragStart', () => { 5 | it('should to be defined', () => { 6 | const { result } = renderHook(() => useDragStart()); 7 | expect(result.current.dragStart).toBe(false); 8 | expect(result.current.setDragStart).toBeDefined(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/hooks/__tests__/use-global-imageDiff-results.test.ts: -------------------------------------------------------------------------------- 1 | import { useGlobalImageDiffResults } from '../use-global-imageDiff-results'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | 4 | describe('useGlobalImageDiffResult', () => { 5 | it('should be defined', () => { 6 | const { result } = renderHook(() => useGlobalImageDiffResults()); 7 | expect(result.current.setImageDiffResult).toBeDefined(); 8 | expect(result.current.imageDiffResult).toStrictEqual([]); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/hooks/__tests__/use-global-screenshot-dispatch.test.ts: -------------------------------------------------------------------------------- 1 | import { useGlobalScreenshotDispatch } from '../use-global-screenshot-dispatch'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | import { _dispatchFuncs } from '../use-global-dispatch'; 4 | 5 | // jest.mock('../use-global-dispatch.ts'); 6 | 7 | describe('useGlobalScreenshotDispatch', () => { 8 | beforeEach(() => { 9 | Object.keys(_dispatchFuncs).forEach((cb) => { 10 | delete _dispatchFuncs[cb]; 11 | }); 12 | }); 13 | 14 | it('should dispatch', () => { 15 | const screenshotDispatchMock = jest.fn(); 16 | renderHook(() => useGlobalScreenshotDispatch(screenshotDispatchMock)); 17 | 18 | expect(Object.keys(_dispatchFuncs)[0]).toStrictEqual('screenshot-dispatch'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/hooks/__tests__/use-story-url.test.ts: -------------------------------------------------------------------------------- 1 | import { useStoryUrl } from '../use-story-url'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | 4 | describe('useStoryUrl', () => { 5 | it('should useStoryUrl', () => { 6 | const { result } = renderHook(() => useStoryUrl()); 7 | expect(result.current.split('?')[1]).toStrictEqual( 8 | 'id=story-id&knob-text=some text', 9 | ); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/hooks/use-addon-state.ts: -------------------------------------------------------------------------------- 1 | import { ADDON_STORAGE_KEY } from '../constants'; 2 | import { AddonState } from '../typings'; 3 | import { useGlobalState } from './use-global-state'; 4 | 5 | export const useAddonState = () => { 6 | const [addonState, setAddonState] = useGlobalState( 7 | ADDON_STORAGE_KEY, 8 | {} as AddonState, 9 | true, 10 | ); 11 | 12 | return { addonState, setAddonState }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/hooks/use-anchor-el.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const useAnchorEl = () => { 4 | const anchorElRef = React.useRef(null); 5 | const [anchorEl, _setAnchorEl] = React.useState(); 6 | 7 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 8 | // @ts-ignore 9 | const setAnchorEl = React.useCallback((e?: any) => { 10 | _setAnchorEl(anchorElRef.current || e.currentTarget); 11 | }, []); 12 | 13 | const clearAnchorEl = React.useCallback(() => { 14 | _setAnchorEl(undefined); 15 | }, []); 16 | 17 | return { anchorEl, anchorElRef, clearAnchorEl, setAnchorEl }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/hooks/use-current-story-data.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useStorybookApi } from '@storybook/api'; 3 | import { StoryData } from '../typings'; 4 | 5 | export const useCurrentStoryData = () => { 6 | const [storyData, setData] = useState(); 7 | 8 | const api = useStorybookApi(); 9 | 10 | const currentStory = (api.getCurrentStoryData() as unknown) as StoryData; 11 | 12 | useEffect(() => { 13 | if (!currentStory) return; 14 | 15 | const data = currentStory; 16 | const fileName = data.parameters.fileName; 17 | 18 | setData({ 19 | ...data, 20 | parameters: { ...data.parameters, fileName: fileName }, 21 | }); 22 | }, [currentStory]); 23 | 24 | return storyData; 25 | }; 26 | -------------------------------------------------------------------------------- /src/hooks/use-custom-theme.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@material-ui/core'; 2 | import React from 'react'; 3 | import { getThemeData } from '../api/client/get-theme-data'; 4 | 5 | export const useCustomTheme = () => { 6 | const [theme, setTheme] = React.useState(); 7 | const isTheme = React.useRef(false); 8 | 9 | const fetchTheme = React.useCallback(async () => { 10 | try { 11 | const resp = await getThemeData(); 12 | if (!isTheme.current && resp) { 13 | setTheme(resp); 14 | } 15 | } catch (error) { 16 | throw new Error((error as Error).message); 17 | } 18 | }, []); 19 | 20 | React.useEffect(() => { 21 | fetchTheme(); 22 | return () => { 23 | isTheme.current = true; 24 | }; 25 | }, [fetchTheme]); 26 | 27 | return { setTheme, theme }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/hooks/use-drag-start.ts: -------------------------------------------------------------------------------- 1 | import { useGlobalState } from './use-global-state'; 2 | 3 | export const useDragStart = () => { 4 | const [dragStart, setDragStart] = useGlobalState('drag-start', false); 5 | return { dragStart, setDragStart }; 6 | }; 7 | -------------------------------------------------------------------------------- /src/hooks/use-global-action-dispatch.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '../store/actions/reducer'; 2 | import { useGlobalDispatch, DispatchType } from './use-global-dispatch'; 3 | 4 | export const useGlobalActionDispatch = (callback?: DispatchType) => { 5 | const { dispatch } = useGlobalDispatch('action-dispatch', callback); 6 | 7 | return { dispatch }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/hooks/use-global-dispatch.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export type DispatchType = (action: T) => void; 5 | 6 | export const _dispatchFuncs: { [id: string]: DispatchType } = {}; 7 | 8 | export function useGlobalDispatch(id: string, callback?: DispatchType) { 9 | if (callback) { 10 | _dispatchFuncs[id] = callback; 11 | } 12 | 13 | const dispatch = useCallback( 14 | (action?: T) => { 15 | if (!_dispatchFuncs[id]) { 16 | throw new Error('Dispatch id not registered yet!'); 17 | } 18 | _dispatchFuncs[id](action); 19 | }, 20 | [id], 21 | ); 22 | 23 | return { dispatch }; 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/use-global-imageDiff-results.ts: -------------------------------------------------------------------------------- 1 | import { useGlobalState } from './use-global-state'; 2 | import { ImageDiffResult } from '../api/typings'; 3 | import { Dispatch, SetStateAction } from 'react'; 4 | 5 | export interface UseGlobalImageDiffResults { 6 | imageDiffResult: ImageDiffResult[]; 7 | setImageDiffResult: Dispatch>; 8 | } 9 | 10 | export const useGlobalImageDiffResults = (): UseGlobalImageDiffResults => { 11 | const [imageDiffResult, setImageDiffResult] = useGlobalState< 12 | ImageDiffResult[] 13 | >('image-diff-results', []); 14 | return { imageDiffResult, setImageDiffResult }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/hooks/use-global-screenshot-dispatch.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '../store/screenshot'; 2 | import { useGlobalDispatch, DispatchType } from './use-global-dispatch'; 3 | 4 | export const useGlobalScreenshotDispatch = ( 5 | callback?: DispatchType, 6 | ) => { 7 | const { dispatch } = useGlobalDispatch( 8 | 'screenshot-dispatch', 9 | callback, 10 | ); 11 | 12 | return { dispatch }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/hooks/use-key-press-fn.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export function useKeyPressFn( 4 | keydown?: (ev: KeyboardEvent) => void, 5 | keyup?: (ev: KeyboardEvent) => void, 6 | disable = false, 7 | ) { 8 | // Add event listeners 9 | useEffect(() => { 10 | if (disable) return undefined; 11 | if (keydown) window.addEventListener('keydown', keydown); 12 | if (keyup) window.addEventListener('keyup', keyup); 13 | // Remove event listeners on cleanup 14 | return () => { 15 | if (keydown) window.removeEventListener('keydown', keydown); 16 | if (keyup) window.removeEventListener('keyup', keyup); 17 | }; 18 | }, [disable, keydown, keyup]); // Empty array ensures that effect is only run on mount and unmount 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/use-key-press.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | import { useKeyPressFn } from './use-key-press-fn'; 3 | 4 | export function useKeyPress(targetKey, disable = false) { 5 | // State for keeping track of whether key is pressed 6 | const [keyPressed, setKeyPressed] = useState(false); 7 | 8 | const downHandler = useCallback( 9 | ({ key }) => { 10 | if (keyPressed) return; 11 | if (key === targetKey) { 12 | setKeyPressed(true); 13 | } 14 | }, 15 | [keyPressed, targetKey], 16 | ); 17 | 18 | const upHandler = useCallback( 19 | ({ key }) => { 20 | if (key === targetKey) { 21 | setKeyPressed(false); 22 | } 23 | }, 24 | [targetKey], 25 | ); 26 | 27 | useKeyPressFn(downHandler, upHandler, disable); 28 | 29 | return keyPressed; 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/use-preview-iframe.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { getPreviewIframe } from '../utils'; 3 | 4 | export const usePreviewIframe = () => { 5 | const iframe = useRef(null); 6 | 7 | if (!iframe.current) { 8 | iframe.current = getPreviewIframe(); 9 | } 10 | 11 | useEffect(() => { 12 | return () => { 13 | iframe.current = undefined; 14 | }; 15 | }, []); 16 | 17 | return iframe.current; 18 | }; 19 | -------------------------------------------------------------------------------- /src/hooks/use-reset-setting.ts: -------------------------------------------------------------------------------- 1 | import { RESET } from '@storybook/addon-knobs/dist/shared'; 2 | import { useStorybookApi } from '@storybook/api'; 3 | import { useGlobalActionDispatch } from './use-global-action-dispatch'; 4 | import { useCurrentStoryData } from './use-current-story-data'; 5 | 6 | export const useResetSetting = () => { 7 | const api = useStorybookApi(); 8 | 9 | const { dispatch } = useGlobalActionDispatch(); 10 | 11 | const data = useCurrentStoryData(); 12 | 13 | const reset = () => { 14 | api.emit(RESET); 15 | dispatch({ type: 'clearCurrentActionSets' }); 16 | dispatch({ storyId: data.id, type: 'deleteTempActionSets' }); 17 | }; 18 | 19 | return reset; 20 | }; 21 | -------------------------------------------------------------------------------- /src/hooks/use-screenshot-options.ts: -------------------------------------------------------------------------------- 1 | import { useGlobalState } from './use-global-state'; 2 | import { ScreenshotOptions } from '../typings'; 3 | import { Dispatch, SetStateAction } from 'react'; 4 | 5 | export const useScreenshotOptions: () => { 6 | screenshotOptions: ScreenshotOptions; 7 | setScreenshotOptions: Dispatch>; 8 | } = () => { 9 | const [screenshotOptions, setScreenshotOptions] = useGlobalState< 10 | ScreenshotOptions 11 | >('screenshotOptions', {}, true); 12 | 13 | return { screenshotOptions, setScreenshotOptions }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/hooks/use-story-url.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useStorybookState } from '@storybook/api'; 3 | import { constructStoryUrl } from '../utils'; 4 | 5 | export const useStoryUrl = () => { 6 | const state = useStorybookState(); 7 | const [url, setUrl] = useState(); 8 | 9 | useEffect(() => { 10 | let newUrl = constructStoryUrl(window.location.host, state.storyId); 11 | 12 | const queryKeys = Object.keys(state.customQueryParams); 13 | if (queryKeys.length > 0) { 14 | const query = queryKeys.map((key) => { 15 | const val = state.customQueryParams[key]; 16 | 17 | return `${key}=${val}`; 18 | }); 19 | newUrl += '&' + query.join('&'); 20 | } 21 | 22 | setUrl(newUrl); 23 | }, [state.customQueryParams, state.storyId]); 24 | 25 | return url; 26 | }; 27 | -------------------------------------------------------------------------------- /src/icons/Browser.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const Browser: React.FC> = (props) => ( 4 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | export { Browser }; 20 | -------------------------------------------------------------------------------- /src/icons/LayoutBottom.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LayoutBottom: React.FC> = (props) => { 4 | return ( 5 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | LayoutBottom.displayName = 'LayoutBottom'; 21 | 22 | export { LayoutBottom }; 23 | -------------------------------------------------------------------------------- /src/icons/LayoutBottomRight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LayoutBottomRight: React.FC> = (props) => { 4 | return ( 5 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | LayoutBottomRight.displayName = 'LayoutBottomRight'; 25 | 26 | export { LayoutBottomRight }; 27 | -------------------------------------------------------------------------------- /src/icons/LayoutRight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LayoutRight: React.FC> = (props) => { 4 | return ( 5 | 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | LayoutRight.displayName = 'LayoutRight'; 22 | 23 | export { LayoutRight }; 24 | -------------------------------------------------------------------------------- /src/icons/__tests__/Browser.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Browser } from '../Browser'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('Browser', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(); 8 | expect(wrapper.exists()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/icons/__tests__/Chrome.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Chrome } from '../Chrome'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('Chrome', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(); 8 | expect(wrapper.exists()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/icons/__tests__/Firefox.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Firefox } from '../Firefox'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('Firefox', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(); 8 | expect(wrapper.exists()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/icons/__tests__/LayoutBottom.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutBottom } from '../LayoutBottom'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('LayoutBottom', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(); 8 | expect(wrapper.exists()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/icons/__tests__/LayoutBottomRight.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutBottomRight } from '../LayoutBottomRight'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('LayoutBottomRight', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(); 8 | expect(wrapper.exists()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/icons/__tests__/LayoutRight.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutRight } from '../LayoutRight'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('LayoutRight', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(); 8 | expect(wrapper.exists()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/icons/__tests__/TestIcon.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TestIcon } from '../TestIcon'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('TestIcon', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(); 8 | expect(wrapper.exists()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/icons/__tests__/Webkit.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Webkit } from '../Webkit'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('Webkit', () => { 6 | it('should render', () => { 7 | const wrapper = shallow(); 8 | expect(wrapper.exists()).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Browser'; 2 | export * from './Chrome'; 3 | export * from './Firefox'; 4 | export * from './Webkit'; 5 | export * from './TestIcon'; 6 | export * from './LayoutRight'; 7 | export * from './LayoutBottom'; 8 | export * from './LayoutBottomRight'; 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/triple-slash-reference 2 | /// 3 | 4 | export * from './run-image-diff'; 5 | export * from './get-screenshots'; 6 | export * from './to-match-screenshots'; 7 | -------------------------------------------------------------------------------- /src/page-extra/clear-input.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'playwright'; 2 | import { ClearInputOptions } from './typings'; 3 | 4 | export async function clearInput( 5 | this: Page, 6 | selector: string, 7 | options: ClearInputOptions = {}, 8 | ) { 9 | const { blur, timeout } = options; 10 | 11 | await this.fill(selector, ''); 12 | 13 | if (blur) { 14 | await this.$eval(selector, (e) => e.blur()); 15 | } 16 | 17 | if (timeout) { 18 | await this.waitForTimeout(options.timeout); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/page-extra/index.ts: -------------------------------------------------------------------------------- 1 | export * from './typings'; 2 | export * from './utils'; 3 | export * from './drag-drop-selector'; 4 | export * from './extend-page'; 5 | export * from './mouse-down-on-selector'; 6 | export * from './mouse-move-to-selector'; 7 | export * from './scroll-selector'; 8 | export * from './set-selector-size'; 9 | export * from './clear-input'; 10 | export * from './selector-mouse-wheel'; 11 | export * from './touch-cancel'; 12 | export * from './touch-move'; 13 | export * from './touch-end'; 14 | export * from './touch-start'; 15 | export * from './mouse-from-to'; 16 | -------------------------------------------------------------------------------- /src/page-extra/mouse-from-to.ts: -------------------------------------------------------------------------------- 1 | import { MouseFromToOptions, Position } from './typings'; 2 | import { Page } from 'playwright'; 3 | 4 | export async function mouseFromTo( 5 | this: Page, 6 | from: Position, 7 | to: Position, 8 | options?: MouseFromToOptions, 9 | ) { 10 | const { skipMouseUp, steps = 1 } = options || {}; 11 | 12 | // move mouse to center of element or specified point 13 | await this.mouse.move(from.x, from.y, { steps }); 14 | 15 | await this.mouse.down(); 16 | 17 | await this.mouse.move(to.x, to.y, { steps }); 18 | 19 | if (!skipMouseUp) await this.mouse.up(); 20 | } 21 | -------------------------------------------------------------------------------- /src/page-extra/scroll-selector.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedPage, Location } from './typings'; 2 | 3 | export async function scrollSelector( 4 | this: ExtendedPage, 5 | selector: string, 6 | scrollProperty: Location, 7 | ) { 8 | await this.waitForSelector(selector); 9 | 10 | const result = await this.$eval( 11 | selector, 12 | (el, points) => { 13 | if (points.top) { 14 | (el as HTMLElement).scrollTop = points.top; 15 | } 16 | if (points.left) { 17 | (el as HTMLElement).scrollLeft = points.left; 18 | } 19 | }, 20 | scrollProperty, 21 | ); 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /src/page-extra/selector-mouse-wheel.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedPage, SelectorMouseWheelOptions } from './typings'; 2 | 3 | export async function selectorMouseWheel( 4 | this: ExtendedPage, 5 | selector: string, 6 | eventInitDict?: SelectorMouseWheelOptions, 7 | ) { 8 | await this.waitForSelector(selector); 9 | 10 | const result = await this.$eval( 11 | selector, 12 | (el, opt) => { 13 | const event = new WheelEvent('wheel', opt as WheelEventInit); 14 | el.dispatchEvent(event); 15 | }, 16 | eventInitDict, 17 | ); 18 | return result; 19 | } 20 | -------------------------------------------------------------------------------- /src/page-extra/set-selector-size.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedPage } from './typings'; 2 | 3 | export async function setSelectorSize( 4 | this: ExtendedPage, 5 | selector: string, 6 | width?: string, 7 | height?: string, 8 | ) { 9 | await this.$eval( 10 | selector, 11 | (el, opt) => { 12 | if (opt.width !== undefined) { 13 | (el as HTMLElement).style.width = opt.width; 14 | } 15 | 16 | if (opt.height !== undefined) { 17 | (el as HTMLElement).style.height = opt.height; 18 | } 19 | }, 20 | { height, width }, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/page-extra/touch-cancel.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'playwright'; 2 | import { Position, PageExtraTouchOptions } from './typings'; 3 | import { dispatchTouchEvent } from './utils/dispatch-touch-event'; 4 | 5 | export async function touchCancel( 6 | this: Page, 7 | selector: string, 8 | page?: Position, 9 | screen?: Position, 10 | client?: Position, 11 | options?: PageExtraTouchOptions, 12 | ) { 13 | await dispatchTouchEvent( 14 | this, 15 | 'touchcancel', 16 | selector, 17 | page, 18 | screen, 19 | client, 20 | options, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/page-extra/touch-end.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'playwright'; 2 | import { Position, PageExtraTouchOptions } from './typings'; 3 | import { dispatchTouchEvent } from './utils/dispatch-touch-event'; 4 | 5 | export async function touchEnd( 6 | this: Page, 7 | selector: string, 8 | page?: Position, 9 | screen?: Position, 10 | client?: Position, 11 | options?: PageExtraTouchOptions, 12 | ) { 13 | await dispatchTouchEvent( 14 | this, 15 | 'touchend', 16 | selector, 17 | page, 18 | screen, 19 | client, 20 | options, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/page-extra/touch-move.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'playwright'; 2 | import { Position, PageExtraTouchOptions } from './typings'; 3 | import { dispatchTouchEvent } from './utils/dispatch-touch-event'; 4 | 5 | export async function touchMove( 6 | this: Page, 7 | selector: string, 8 | page?: Position, 9 | screen?: Position, 10 | client?: Position, 11 | options?: PageExtraTouchOptions, 12 | ) { 13 | await dispatchTouchEvent( 14 | this, 15 | 'touchmove', 16 | selector, 17 | page, 18 | screen, 19 | client, 20 | options, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/page-extra/touch-start.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'playwright'; 2 | import { Position, PageExtraTouchOptions } from './typings'; 3 | import { dispatchTouchEvent } from './utils/dispatch-touch-event'; 4 | 5 | export async function touchStart( 6 | this: Page, 7 | selector: string, 8 | page?: Position, 9 | screen?: Position, 10 | client?: Position, 11 | options?: PageExtraTouchOptions, 12 | ) { 13 | await dispatchTouchEvent( 14 | this, 15 | 'touchstart', 16 | selector, 17 | page, 18 | screen, 19 | client, 20 | options, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/page-extra/typings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './extra'; 2 | -------------------------------------------------------------------------------- /src/page-extra/utils/get-boundingBox.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'playwright'; 2 | import { ElementHandleBoundingBox } from '../typings'; 3 | 4 | export const getBoundingBox = async ( 5 | page: Page, 6 | selector: string, 7 | ): Promise => { 8 | await page.waitForSelector(selector); 9 | const elm = await page.$(selector); 10 | if (!elm) throw new Error('Unable to find selector!'); 11 | const box = await elm.boundingBox(); 12 | if (!box) throw new Error('Unable to get boundingBox!'); 13 | return box; 14 | }; 15 | -------------------------------------------------------------------------------- /src/page-extra/utils/get-point-by-direction.ts: -------------------------------------------------------------------------------- 1 | import { Position } from '../typings'; 2 | 3 | export function getPointByDirection( 4 | defaultVal: number, 5 | direction: 'x' | 'y', 6 | providedPoint?: Position, 7 | ): number { 8 | if (!providedPoint || !providedPoint[direction]) return defaultVal; 9 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 10 | return providedPoint[direction]!; 11 | } 12 | -------------------------------------------------------------------------------- /src/page-extra/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-boundingBox'; 2 | export * from './get-point-by-direction'; 3 | export * from './dispatch-touch-event'; 4 | -------------------------------------------------------------------------------- /src/store/actions/__mocks__/ActionContext.tsx: -------------------------------------------------------------------------------- 1 | import { getActionSchemaData } from '../../../../__test_data__'; 2 | import { mocked } from 'ts-jest/utils'; 3 | import { ReducerState } from '../../../../__manual_mocks__/store/action/context'; 4 | 5 | export const useActionContext = jest.fn(); 6 | export const useActionDispatchContext = jest.fn(); 7 | 8 | const mockData = ({ 9 | actionSchema: getActionSchemaData(), 10 | } as unknown) as ReducerState; 11 | 12 | const useActionContextMock = mocked(useActionContext); 13 | useActionContextMock.mockImplementation(() => mockData); 14 | -------------------------------------------------------------------------------- /src/store/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionContext'; 2 | -------------------------------------------------------------------------------- /src/store/actions/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './is-same-actions'; 2 | -------------------------------------------------------------------------------- /src/store/actions/utils/is-same-actions.ts: -------------------------------------------------------------------------------- 1 | import { StoryAction } from '../../../typings'; 2 | import equal from 'fast-deep-equal'; 3 | 4 | function actionsWithoutId(actions: Partial[]) { 5 | return actions.map((action) => { 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | const { id, ...rest } = action; 8 | return rest; 9 | }); 10 | } 11 | 12 | export const isSameActions = ( 13 | a: Partial[], 14 | b: Partial[], 15 | ) => { 16 | if (a.length !== b.length) return false; 17 | 18 | const newA = actionsWithoutId(a); 19 | 20 | const newB = actionsWithoutId(b); 21 | 22 | return equal(newA, newB); 23 | }; 24 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | -------------------------------------------------------------------------------- /src/store/screenshot/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | export * from './reducer'; 3 | -------------------------------------------------------------------------------- /src/typings/action-schema.ts: -------------------------------------------------------------------------------- 1 | import { Definition } from 'ts-to-json'; 2 | 3 | export type ActionSchema = Definition; 4 | 5 | export type ActionSchemaList = { 6 | [key: string]: ActionSchema; 7 | }; 8 | -------------------------------------------------------------------------------- /src/typings/addon-state.ts: -------------------------------------------------------------------------------- 1 | import { BrowserTypes } from './screenshot'; 2 | 3 | export type ActiveBrowser = { 4 | [key in BrowserTypes]?: boolean; 5 | }; 6 | 7 | export type ScreenShotViewPanel = 'main' | 'dialog'; 8 | 9 | export type DisabledBrowserView = { 10 | [key in ScreenShotViewPanel]?: ActiveBrowser; 11 | }; 12 | 13 | export interface AddonState { 14 | placement: 'auto' | 'bottom' | 'right' | string; 15 | previewPanelSize: number; 16 | disabledBrowser: DisabledBrowserView; 17 | previewPanelEnabled: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/typings/compare-screenshot.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotPathInfo } from '../api/server/utils/get-screenshot-paths'; 2 | import { ImageDiffResult } from '../api/typings'; 3 | import { RequestData } from './request'; 4 | import { 5 | BrowserTypes, 6 | ScreenshotImageData, 7 | ScreenshotInfo, 8 | } from './screenshot'; 9 | 10 | export interface BaseImageInfo extends ScreenshotPathInfo { 11 | buffer: Buffer; 12 | base64: string; 13 | } 14 | 15 | export interface CompareScreenshotParams extends ScreenshotInfo, RequestData { 16 | screenshot: ScreenshotImageData; 17 | browserType: BrowserTypes; 18 | baseImage: BaseImageInfo; 19 | } 20 | 21 | export interface CompareScreenshotReturnType 22 | extends Required>, 23 | Omit { 24 | diffImageString?: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/typings/control.ts: -------------------------------------------------------------------------------- 1 | import { ControlTypes } from '.'; 2 | import { OptionsKnobOptionsDisplay } from '@storybook/addon-knobs/dist/components/types/Options'; 3 | 4 | export interface ControlProps { 5 | label: string; 6 | type: ControlTypes; 7 | value?: unknown; 8 | onChange: (value: unknown) => void; 9 | options?: string[]; 10 | display?: OptionsKnobOptionsDisplay; 11 | description?: string; 12 | appendValueToTitle: boolean; 13 | onAppendValueToTitle: () => void; 14 | isRequired: boolean; 15 | defaultValue?: unknown; 16 | } 17 | -------------------------------------------------------------------------------- /src/typings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addon-state'; 2 | export * from './control'; 3 | export * from './knobs'; 4 | export * from './screenshot'; 5 | export * from './selector'; 6 | export * from './story-action'; 7 | export * from './action-schema'; 8 | export * from './storybook'; 9 | export * from './story-data'; 10 | export * from './story-info'; 11 | export * from './config'; 12 | -------------------------------------------------------------------------------- /src/typings/knobs.ts: -------------------------------------------------------------------------------- 1 | import { KnobStoreKnob } from '@storybook/addon-knobs/dist/KnobStore'; 2 | 3 | export type KnobStore = Record; 4 | 5 | export { KnobStoreKnob }; 6 | -------------------------------------------------------------------------------- /src/typings/request.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotTestTargetType } from './screenshot'; 2 | 3 | export interface RequestData { 4 | requestId: string; 5 | requestType?: ScreenshotTestTargetType; 6 | } 7 | -------------------------------------------------------------------------------- /src/typings/selector.ts: -------------------------------------------------------------------------------- 1 | export interface SelectorState { 2 | path?: string; 3 | x?: number; 4 | y?: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/typings/story-action.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'playwright'; 2 | import { KnobType } from '@storybook/addon-knobs/dist/components/types'; 3 | export type StoryActionPosition = { x: number; y: number }; 4 | 5 | export type ControlTypes = KnobType; 6 | 7 | export interface StoryAction { 8 | id: string; 9 | name: string; 10 | labe?: string; 11 | args?: { [key: string]: unknown }; 12 | subtitleItems?: string[]; 13 | run?: (page: T, selector: string) => Promise; 14 | } 15 | 16 | export interface Stories { 17 | [storyId: string]: { actionSets: ActionSet[] }; 18 | } 19 | 20 | export interface ActionSet { 21 | id: string; 22 | title: string; 23 | actions: StoryAction[]; 24 | temp?: boolean; 25 | } 26 | 27 | export interface FavouriteActionSet extends ActionSet { 28 | visibleTo?: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/typings/story-data.ts: -------------------------------------------------------------------------------- 1 | import { ActionSet } from './story-action'; 2 | import { 3 | ScreenshotData, 4 | BrowserContextOptions, 5 | ScreenshotOptions, 6 | } from './screenshot'; 7 | 8 | export interface PlaywrightStoryData { 9 | actionSets?: ActionSet[]; 10 | screenshots?: ScreenshotData[]; 11 | } 12 | 13 | export type StoryBrowserOptions = { [id: string]: BrowserContextOptions }; 14 | export type StoryScreenshotOptions = { [id: string]: ScreenshotOptions }; 15 | export type PlaywrightDataStories = { [id: string]: PlaywrightStoryData }; 16 | 17 | export interface StoryOptions { 18 | browserOptions?: StoryBrowserOptions; 19 | screenshotOptions?: StoryScreenshotOptions; 20 | } 21 | 22 | export interface PlaywrightData extends StoryOptions { 23 | stories?: PlaywrightDataStories; 24 | version?: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/typings/story-info.ts: -------------------------------------------------------------------------------- 1 | export interface StoryInfo { 2 | fileName?: string; 3 | storyId: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/typings/storybook.ts: -------------------------------------------------------------------------------- 1 | // import { StoriesRaw } from '@storybook/api/dist/modules/stories'; 2 | 3 | export type StoryData = { 4 | id: string; 5 | name: string; 6 | kind: string; 7 | children: string[]; 8 | parameters: { 9 | fileName: string; 10 | options: { 11 | // hierarchyRootSeparator: RegExp; 12 | // hierarchySeparator: RegExp; 13 | showRoots?: boolean; 14 | [key: string]: unknown | undefined; 15 | }; 16 | [parameterName: string]: unknown; 17 | }; 18 | isLeaf: boolean; 19 | parent: string; 20 | }; 21 | -------------------------------------------------------------------------------- /src/utils/__mocks__/get-playwright-config-files.ts: -------------------------------------------------------------------------------- 1 | export const getPlaywrightConfigFiles = jest.fn(); 2 | 3 | getPlaywrightConfigFiles.mockImplementation(() => [ 4 | './stories/test.stories.playwright.json', 5 | './stories/test-2.stories.playwright.json', 6 | ]); 7 | -------------------------------------------------------------------------------- /src/utils/__mocks__/valid-action.ts: -------------------------------------------------------------------------------- 1 | export const validAction = jest.fn(); 2 | export const validateActionList = jest.fn(); 3 | 4 | validAction.mockImplementation(() => undefined); 5 | -------------------------------------------------------------------------------- /src/utils/__tests__/capitalize.test.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from '../capitalize'; 2 | 3 | describe('capitalize', () => { 4 | it('should capitalize', () => { 5 | expect(capitalize('foo')).toBe('Foo'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/utils/__tests__/combine-reducer.test.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from '../combine-reducer'; 2 | 3 | describe('combineReducers', () => { 4 | it('should return state', () => { 5 | const reducer1 = (state: unknown) => state; 6 | const reducer2 = (state: unknown) => state; 7 | expect( 8 | combineReducers(reducer1, reducer2)({ old: true }, {}), 9 | ).toStrictEqual({ 10 | old: true, 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/utils/__tests__/get-device-info.test.ts: -------------------------------------------------------------------------------- 1 | import { getDeviceInfo } from '../get-device-info'; 2 | 3 | describe('getDeviceInfo', () => { 4 | it('should return nothing', () => { 5 | expect(getDeviceInfo()).not.toBeDefined(); 6 | }); 7 | it('should device have info', () => { 8 | expect(getDeviceInfo('Blackberry PlayBook')).toBeDefined(); 9 | }); 10 | it('should return nothing if device not found', () => { 11 | expect(getDeviceInfo('Blackberry PlayBook 2')).not.toBeDefined(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/utils/__tests__/get-raw-stories.test.ts: -------------------------------------------------------------------------------- 1 | import { getRawStories } from '../get-raw-stories'; 2 | 3 | jest.mock('../../utils/get-preview-iframe.ts'); 4 | 5 | describe('getRawStories', () => { 6 | it('should have value', () => { 7 | expect(getRawStories()).toHaveLength(2); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/utils/__tests__/get-schema.test.ts: -------------------------------------------------------------------------------- 1 | import { getActionSchema } from '../get-schema'; 2 | import { getActionSchemaData } from '../../../__test_data__'; 3 | 4 | describe('getActionSchema', () => { 5 | it('should return nothing', () => { 6 | expect(getActionSchema(getActionSchemaData(), '')).not.toBeDefined(); 7 | }); 8 | 9 | it('should return nothing if invalid path given', () => { 10 | expect( 11 | getActionSchema(getActionSchemaData(), 'invalid-key'), 12 | ).not.toBeDefined(); 13 | }); 14 | 15 | it('should return action', () => { 16 | expect(getActionSchema(getActionSchemaData(), 'click')).toBeDefined(); 17 | }); 18 | 19 | it('should return nested action', () => { 20 | expect(getActionSchema(getActionSchemaData(), 'mouse.click')).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/utils/__tests__/get-story-function.test.ts: -------------------------------------------------------------------------------- 1 | import { getStoryFunction } from '../get-story-function'; 2 | 3 | jest.mock('../../utils/get-preview-iframe.ts'); 4 | 5 | describe('getStoryFunction', () => { 6 | beforeEach(() => { 7 | jest.clearAllMocks(); 8 | }); 9 | 10 | it('should return func', () => { 11 | expect(typeof getStoryFunction('story-id')).toBe('function'); 12 | }); 13 | 14 | it('should return func if story not exist', () => { 15 | expect(getStoryFunction('bad-story-id')).toBe(undefined); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/utils/__tests__/is-story-json-file.test.ts: -------------------------------------------------------------------------------- 1 | import { isStoryJsonFile } from '../is-story-json-file'; 2 | 3 | describe('isStoryJsonFile', () => { 4 | it('should be valid', () => { 5 | expect( 6 | isStoryJsonFile('test.stories.playwright.json', 'test.stories.tsx'), 7 | ).toBeTruthy(); 8 | expect( 9 | isStoryJsonFile('test.stories.playwright.json', 'test.stories.js'), 10 | ).toBeTruthy(); 11 | }); 12 | it('should not be valid', () => { 13 | expect( 14 | isStoryJsonFile('test1.stories.playwright.json', 'test.stories.tsx'), 15 | ).toBeFalsy(); 16 | }); 17 | 18 | it('should be valid if file names are strictly equal', () => { 19 | expect( 20 | isStoryJsonFile('test.stories.tsx', 'test.stories.tsx'), 21 | ).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/utils/capitalize.ts: -------------------------------------------------------------------------------- 1 | export const capitalize = (str: string) => { 2 | return str.charAt(0).toUpperCase() + str.slice(1); 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/combine-reducer.ts: -------------------------------------------------------------------------------- 1 | export const combineReducers = (...reducers) => (prevState, value) => 2 | reducers.reduce((newState, reducer) => reducer(newState, value), prevState); 3 | -------------------------------------------------------------------------------- /src/utils/construct-story-url.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotProp } from '../typings'; 2 | 3 | import normalize from 'normalize-url'; 4 | import { knobsToQuerystring } from './knobs-to-querystring'; 5 | import { parse } from 'url'; 6 | import { resolve } from 'path'; 7 | import ip from 'ip'; 8 | 9 | export const constructStoryUrl = ( 10 | endpoint: string, 11 | id: string, 12 | props?: ScreenshotProp, 13 | ) => { 14 | const parsedEndpoint = parse(endpoint); 15 | const normalized = 16 | parsedEndpoint.hostname || ip.isV4Format(endpoint) 17 | ? normalize(endpoint) 18 | : 'file:///' + resolve(endpoint); 19 | 20 | let storyUrl = `${normalized}/iframe.html?id=${id}`; 21 | 22 | if (props) { 23 | storyUrl = `${storyUrl}&${knobsToQuerystring(props)}`; 24 | } 25 | 26 | return storyUrl.replace(/\\/g, '/'); 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/find-selector.ts: -------------------------------------------------------------------------------- 1 | import { getPreviewIframe } from './get-preview-iframe'; 2 | 3 | export const findSelector = (selector: string) => { 4 | const element = 5 | getPreviewIframe().contentWindow.document.querySelector(selector); 6 | return element; 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/get-device-info.ts: -------------------------------------------------------------------------------- 1 | import DeviceDescriptors from '../data/deviceDescriptorsSource.json'; 2 | import { BrowserContextOptions } from '../typings'; 3 | 4 | export const getDeviceInfo = ( 5 | deviceName?: string, 6 | ): BrowserContextOptions | undefined => { 7 | if (!deviceName) return undefined; 8 | const device = DeviceDescriptors[deviceName] as BrowserContextOptions; 9 | if (!device) return undefined; 10 | device.deviceName = deviceName; 11 | return device; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/get-iframe-document.ts: -------------------------------------------------------------------------------- 1 | export const getIframeDocument = (iframe: HTMLIFrameElement) => { 2 | return iframe.contentDocument || iframe.contentWindow.document; 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/get-iframe-scroll-position.ts: -------------------------------------------------------------------------------- 1 | import { getIframeDocument } from './get-iframe-document'; 2 | 3 | export const getIframeScrollPosition = (iframe: HTMLIFrameElement) => { 4 | const iframeDocument = getIframeDocument(iframe); 5 | const scrollTop = 6 | iframeDocument.defaultView.pageYOffset || 7 | iframeDocument.documentElement.scrollTop; 8 | const scrollLeft = 9 | iframeDocument.defaultView.pageXOffset || 10 | iframeDocument.documentElement.scrollLeft; 11 | 12 | return { scrollLeft, scrollTop }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/get-image-diff-messages.ts: -------------------------------------------------------------------------------- 1 | import { ImageDiffResult } from '../api/typings'; 2 | 3 | export const getImageDiffMessages = (result: ImageDiffResult) => { 4 | if (result.error) return result.error; 5 | 6 | if (result.diffSize) { 7 | return `Expected image to be the same size as the snapshot (${result.imageDimensions.baselineWidth}x${result.imageDimensions.baselineHeight}), but was different (${result.imageDimensions.receivedWidth}x${result.imageDimensions.receivedHeight}).`; 8 | } 9 | 10 | if (result.diffRatio) { 11 | const differencePercentage = result.diffRatio * 100; 12 | return `Expected image to match or be a close match to snapshot but was ${differencePercentage}% different from snapshot (${result.diffPixelCount} differing pixels).`; 13 | } 14 | return ''; 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/get-playwright-config-files.ts: -------------------------------------------------------------------------------- 1 | import glob from 'fast-glob'; 2 | 3 | export const getPlaywrightConfigFiles = async (configPath?: string | '*') => { 4 | const files = await glob([ 5 | configPath && configPath !== '*' ? configPath : '**/*.playwright.json', 6 | '!node_modules/**', 7 | ]); 8 | 9 | return files; 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/get-preview-iframe.ts: -------------------------------------------------------------------------------- 1 | export const getPreviewIframe = () => { 2 | const iframe = document.body.querySelector( 3 | '#storybook-preview-iframe', 4 | ); 5 | return iframe; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/get-raw-stories.ts: -------------------------------------------------------------------------------- 1 | import { getPreviewIframe } from '../utils'; 2 | import { StoreItem } from '@storybook/client-api/dist/ts3.9'; 3 | 4 | interface RequiredContext { 5 | __STORYBOOK_CLIENT_API__: { 6 | raw: () => StoreItem[]; 7 | }; 8 | } 9 | 10 | export const getRawStories = (): StoreItem[] | undefined => { 11 | const iframeWindow = getPreviewIframe() 12 | .contentWindow as unknown as RequiredContext; 13 | if (!iframeWindow || !iframeWindow.__STORYBOOK_CLIENT_API__) return undefined; 14 | return iframeWindow.__STORYBOOK_CLIENT_API__.raw(); 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/get-schema.ts: -------------------------------------------------------------------------------- 1 | import { ActionSchema } from '../typings'; 2 | import { get } from 'object-path-immutable'; 3 | 4 | export const getActionSchema = ( 5 | schema: ActionSchema, 6 | key: string, 7 | ): ActionSchema | undefined => { 8 | if (!key) return undefined; 9 | 10 | const extendedKey = key.split('.').join('.properties.'); 11 | const actionSchema = get(schema, extendedKey); 12 | return actionSchema; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/get-story-function.ts: -------------------------------------------------------------------------------- 1 | import { getRawStories } from './get-raw-stories'; 2 | 3 | export const getStoryFunction = (storyId: string): any => { 4 | const raw = getRawStories(); 5 | if (!raw) return undefined; 6 | const story = raw.find((x) => x.id === storyId); 7 | if (!story) return undefined; 8 | 9 | return story.getOriginal(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './capitalize'; 2 | export * from './find-selector'; 3 | export * from './get-preview-iframe'; 4 | export * from './get-schema'; 5 | export * from './is-valid-selector'; 6 | export * from './construct-story-url'; 7 | export * from './get-action-args'; 8 | export * from './valid-action'; 9 | export * from './combine-reducer'; 10 | export * from './get-image-diff-messages'; 11 | export * from './knobs-to-querystring'; 12 | export * from './get-device-info'; 13 | export * from './get-story-function'; 14 | export * from './get-raw-stories'; 15 | export * from './is-story-json-file'; 16 | export * from './get-iframe-document'; 17 | export * from './get-iframe-scroll-position'; 18 | -------------------------------------------------------------------------------- /src/utils/is-story-json-file.ts: -------------------------------------------------------------------------------- 1 | export const isStoryJsonFile = ( 2 | jsonOrStoryFileName: string, 3 | storyFileName: string, 4 | ) => { 5 | if (jsonOrStoryFileName === storyFileName) return true; 6 | const jsonName = jsonOrStoryFileName.split('.').slice(0, -2).join('.'); 7 | const storyName = storyFileName.split('.').slice(0, -1).join('.'); 8 | return storyName.endsWith(jsonName); 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/is-valid-selector.ts: -------------------------------------------------------------------------------- 1 | import { findSelector } from './find-selector'; 2 | 3 | export const isValidSelector = (selector: string) => { 4 | try { 5 | if (!selector || findSelector(selector) === null) { 6 | return false; 7 | } else { 8 | return true; 9 | } 10 | } catch (error) { 11 | return false; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/knobs-to-querystring.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotProp } from '../typings'; 2 | 3 | export const knobsToQuerystring = (props?: ScreenshotProp) => { 4 | if (!props) return ''; 5 | 6 | const keys = Object.keys(props); 7 | 8 | const propQuery = keys.map((prop) => { 9 | const propVal = props[prop]; 10 | if (Array.isArray(propVal)) { 11 | if(propVal.length>0&& typeof propVal[0]==='object') 12 | { 13 | return `knob-${prop}=${JSON.stringify(propVal)}`; 14 | }else{ 15 | return `knob-${prop}=${propVal.join(',')}`; 16 | } 17 | } 18 | if (typeof propVal === 'object') { 19 | return `knob-${prop}=${JSON.stringify(propVal)}`; 20 | } 21 | return `knob-${prop}=${propVal}`; 22 | }); 23 | return encodeURI(propQuery.join('&')); 24 | }; 25 | -------------------------------------------------------------------------------- /stories/InputWheel.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { InputWheel } from './InputWheel'; 4 | 5 | export default { component: InputWheel, title: 'InputWheel' }; 6 | 7 | export const withInputWheel = () => { 8 | return ; 9 | }; 10 | -------------------------------------------------------------------------------- /stories/__screenshots__/boxes-with-default-should-allow-last-screenshot-if-last-action-is-not-take-element-screenshot-chromium-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccpu/storybook-addon-playwright/e4d6d001af01fe145e2b2cc2fbdbec07d5c7dd39/stories/__screenshots__/boxes-with-default-should-allow-last-screenshot-if-last-action-is-not-take-element-screenshot-chromium-snap.png -------------------------------------------------------------------------------- /stories/__screenshots__/boxes-with-default-should-take-screen-shot-by-element-id-chromium-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccpu/storybook-addon-playwright/e4d6d001af01fe145e2b2cc2fbdbec07d5c7dd39/stories/__screenshots__/boxes-with-default-should-take-screen-shot-by-element-id-chromium-snap.png -------------------------------------------------------------------------------- /stories/__screenshots__/inputwheel-with-input-wheel-should-wait-for-input-value-to-change-to-1-and-change-input-value-afterwards-chromium-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccpu/storybook-addon-playwright/e4d6d001af01fe145e2b2cc2fbdbec07d5c7dd39/stories/__screenshots__/inputwheel-with-input-wheel-should-wait-for-input-value-to-change-to-1-and-change-input-value-afterwards-chromium-snap.png -------------------------------------------------------------------------------- /stories/__screenshots__/mouse-with-draggable-should-drag-chromium-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccpu/storybook-addon-playwright/e4d6d001af01fe145e2b2cc2fbdbec07d5c7dd39/stories/__screenshots__/mouse-with-draggable-should-drag-chromium-snap.png -------------------------------------------------------------------------------- /stories/__screenshots__/mouse-with-tippy-click-chromium-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccpu/storybook-addon-playwright/e4d6d001af01fe145e2b2cc2fbdbec07d5c7dd39/stories/__screenshots__/mouse-with-tippy-click-chromium-snap.png -------------------------------------------------------------------------------- /stories/__screenshots__/mouse-with-tippy-should-hover-chromium-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccpu/storybook-addon-playwright/e4d6d001af01fe145e2b2cc2fbdbec07d5c7dd39/stories/__screenshots__/mouse-with-tippy-should-hover-chromium-snap.png -------------------------------------------------------------------------------- /stories/__screenshots__/touch-with-default-should-handle-touch-events-chromium-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccpu/storybook-addon-playwright/e4d6d001af01fe145e2b2cc2fbdbec07d5c7dd39/stories/__screenshots__/touch-with-default-should-handle-touch-events-chromium-snap.png -------------------------------------------------------------------------------- /stories/__tests__/first.stories.test.tsx: -------------------------------------------------------------------------------- 1 | import { setupPlaywright } from '../../.storybook/setup-playwright'; 2 | import { toMatchScreenshots } from '../../dist/to-match-screenshots'; 3 | 4 | expect.extend({ toMatchScreenshots }); 5 | 6 | //! to run uncomment testPathIgnorePatterns: ['./stories/*'] in jest.config.js 7 | 8 | describe('should pass', () => { 9 | beforeAll(async () => { 10 | await setupPlaywright(); 11 | }); 12 | 13 | it('should pass', async () => { 14 | await expect('../first.stories.playwright.json').toMatchScreenshots(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "removeComments": false 7 | }, 8 | "include": ["src/**/*", "typings/**/*"], 9 | "exclude": [ 10 | "node_modules", 11 | "**/node_modules/*", 12 | "**/__tests__/*", 13 | "**/__mocks__/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /typings/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 3 | import { MatchImageSnapshotOptions } from 'jest-image-snapshot'; 4 | import 'jest-extended'; 5 | 6 | declare global { 7 | namespace jest { 8 | interface Matchers { 9 | toMatchScreenshots(options?: MatchImageSnapshotOptions): R; 10 | } 11 | } 12 | interface Window { 13 | __visible_snackbar_messages__: { [message: string]: boolean }; 14 | } 15 | } 16 | --------------------------------------------------------------------------------