├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yaml │ ├── feature_request.yaml │ └── project_spec.md ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── commit-lint.yml │ ├── release-please.yml │ ├── release-test-version.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .release-please-manifest.json ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── grafana-labs-dark-theme.svg ├── grafana-labs.svg ├── k6-studio-screenshot.png ├── logo-dark-theme.svg └── logo.svg ├── catalog-info.yaml ├── entitlements.plist ├── extension └── src │ ├── background │ ├── index.ts │ ├── navigation.ts │ └── routing.ts │ ├── env.d.ts │ ├── frontend │ ├── index.ts │ ├── routing.ts │ └── view │ │ ├── Anchor.tsx │ │ ├── ElementInspector │ │ ├── ElementInspector.hooks.ts │ │ ├── ElementInspector.tsx │ │ ├── ElementInspector.utils.ts │ │ ├── ElementMenu.tsx │ │ ├── ElementPopover.tsx │ │ ├── assertions │ │ │ ├── AssertionEditor.tsx │ │ │ ├── AssertionForm.tsx │ │ │ ├── TextAssertionEditor.tsx │ │ │ ├── VisibilityAssertionEditor.tsx │ │ │ └── types.ts │ │ └── index.ts │ │ ├── EventDrawer.tsx │ │ ├── GlobalStyles.tsx │ │ ├── InBrowserControls.tsx │ │ ├── Overlay.tsx │ │ ├── RemoteHighlights.hooks.ts │ │ ├── RemoteHighlights.tsx │ │ ├── TextSelectionPopover.hooks.ts │ │ ├── TextSelectionPopover.tsx │ │ ├── TextSelectionPopover.types.ts │ │ ├── ToolBox.tsx │ │ ├── hooks │ │ ├── useDebouncedValue.tsx │ │ ├── useEscape.tsx │ │ ├── useHighlightDebounce.tsx │ │ └── usePreventClick.tsx │ │ ├── index.tsx │ │ ├── store.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── messaging │ ├── index.ts │ ├── transports │ │ ├── background.ts │ │ ├── buffered.ts │ │ ├── null.ts │ │ ├── port.ts │ │ ├── transport.ts │ │ ├── webSocket.ts │ │ └── webSocketServer.ts │ └── types.ts │ ├── selectors.ts │ └── utils │ └── events.ts ├── forge.config.ts ├── forge.env.d.ts ├── index.html ├── install-k6.js ├── package-lock.json ├── package.json ├── release-please-config.json ├── resources ├── checks_snippet.js ├── group_snippet.js ├── icons │ ├── logo.icns │ ├── logo.ico │ └── logo.png ├── json_output.py ├── linux │ ├── arm64 │ │ └── k6-studio-proxy │ └── x86_64 │ │ └── k6-studio-proxy ├── logo-splashscreen-dark.svg ├── logo-splashscreen.svg ├── mac │ ├── arm64 │ │ └── k6-studio-proxy │ └── x86_64 │ │ └── k6-studio-proxy ├── splashscreen.html └── win │ └── x86_64 │ └── k6-studio-proxy.exe ├── src ├── App.tsx ├── AppRoutes.tsx ├── ErrorElement.tsx ├── assets │ ├── fonts │ │ └── Inter │ │ │ └── InterVariable.woff2 │ ├── grot-crashed.svg │ ├── grot-tea.svg │ ├── grot.svg │ ├── logo-dark.svg │ └── logo.svg ├── codegen │ ├── browser │ │ ├── __snapshots__ │ │ │ └── browser │ │ │ │ ├── assertions │ │ │ │ ├── element-contains-text.ts │ │ │ │ ├── element-is-hidden.ts │ │ │ │ └── element-is-visible.ts │ │ │ │ ├── check-element.ts │ │ │ │ ├── click-element-with-modifier-keys.ts │ │ │ │ ├── click-element.ts │ │ │ │ ├── empty-browser-test.ts │ │ │ │ ├── goto-url.ts │ │ │ │ ├── reload-page.ts │ │ │ │ ├── right-click-element.ts │ │ │ │ ├── select-multiple-options-on-element.ts │ │ │ │ ├── select-single-option-on-element.ts │ │ │ │ ├── type-text-on-element.ts │ │ │ │ └── uncheck-element.ts │ │ ├── code │ │ │ ├── comment.ts │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── options.ts │ │ │ └── scenario.ts │ │ ├── codegen.test.ts │ │ ├── codegen.ts │ │ ├── formatting │ │ │ ├── formatter.ts │ │ │ └── spacing.ts │ │ ├── graph.ts │ │ ├── index.ts │ │ ├── intermediate │ │ │ ├── ast.ts │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ └── variables.ts │ │ ├── test.ts │ │ └── types.ts │ ├── codegen.test.ts │ ├── codegen.ts │ ├── codegen.utils.test.ts │ ├── codegen.utils.ts │ ├── estree │ │ ├── declarations.ts │ │ ├── expressions.ts │ │ ├── index.ts │ │ ├── modules.ts │ │ ├── nodes.ts │ │ ├── statements.ts │ │ ├── traverse.test.ts │ │ ├── traverse.ts │ │ ├── types.ts │ │ └── typescript-estree.d.ts │ ├── imports.test.ts │ ├── imports.ts │ ├── index.ts │ ├── options.test.ts │ └── options.ts ├── components │ ├── BrowserEventList │ │ ├── BrowserEventList.tsx │ │ ├── EventDescription │ │ │ ├── AssertDescription.tsx │ │ │ ├── ClickDescription.tsx │ │ │ ├── EventDescription.tsx │ │ │ ├── InputChangeDescription.tsx │ │ │ ├── PageNavigationDescription.tsx │ │ │ ├── Selector.tsx │ │ │ └── index.ts │ │ ├── EventIcon.tsx │ │ └── index.ts │ ├── ButtonWithTooltip.tsx │ ├── Collapsible.tsx │ ├── CollapsibleSection │ │ ├── CollapsibleSection.styles.css │ │ ├── CollapsibleSection.tsx │ │ └── index.ts │ ├── DevToolsDialog.tsx │ ├── DurationInput.tsx │ ├── EmptyMessage.tsx │ ├── ExternalLink.tsx │ ├── Feature.tsx │ ├── FileNameHeader.tsx │ ├── FileTree │ │ ├── File.tsx │ │ ├── FileContextMenu.tsx │ │ ├── FileList.tsx │ │ ├── FileTree.tsx │ │ ├── InlineEditor.tsx │ │ ├── index.ts │ │ └── types.ts │ ├── Form │ │ ├── ControlledReactSelect.tsx │ │ ├── ControlledSelect.tsx │ │ ├── ControllerRadioGroup.tsx │ │ ├── FieldError.tsx │ │ ├── FieldGroup.tsx │ │ ├── FileUploadInput.tsx │ │ └── index.ts │ ├── GhostButton.tsx │ ├── HighlightedText.tsx │ ├── JsonPreview.tsx │ ├── Label.tsx │ ├── Layout │ │ ├── ActivityBar │ │ │ ├── ActivityBar.tsx │ │ │ ├── HelpButton.tsx │ │ │ ├── NavIconButton.tsx │ │ │ ├── Profile.tsx │ │ │ ├── ProxyStatusIndicator.tsx │ │ │ ├── SettingsButton.tsx │ │ │ ├── VersionLabel.tsx │ │ │ └── index.ts │ │ ├── Layout.tsx │ │ ├── Sidebar │ │ │ ├── Sidebar.hooks.ts │ │ │ ├── Sidebar.tsx │ │ │ └── index.ts │ │ ├── View.tsx │ │ └── ViewHeading.tsx │ ├── LogView │ │ ├── LogView.tsx │ │ └── index.ts │ ├── MethodBadge.tsx │ ├── Monaco │ │ ├── CodeEditor.tsx │ │ ├── ConstrainerCodeEditor.tsx │ │ ├── DiffEditor.tsx │ │ ├── EditorToolbar.tsx │ │ ├── ReactMonacoEditor.hooks.ts │ │ ├── ReactMonacoEditor.tsx │ │ ├── ReadOnlyEditor.tsx │ │ ├── defaultOptions.ts │ │ ├── languages │ │ │ └── log.ts │ │ ├── setMonacoEnv.ts │ │ ├── themes │ │ │ ├── k6StudioDark.ts │ │ │ └── k6StudioLight.ts │ │ └── useShouldEnableWordWrap.ts │ ├── PopoverDialogs.tsx │ ├── Profile │ │ ├── Avatar.tsx │ │ ├── GrafanaCloudSignIn │ │ │ ├── AuthenticationMessage.tsx │ │ │ ├── AuthorizationDenied.tsx │ │ │ ├── AwaitingAuthorization.tsx │ │ │ ├── FetchingStacks.tsx │ │ │ ├── FetchingToken.tsx │ │ │ ├── Initializing.tsx │ │ │ ├── SelectingStack.tsx │ │ │ ├── StackLoginRequired.tsx │ │ │ ├── TimedOut.tsx │ │ │ ├── UnexpectedError.tsx │ │ │ └── index.tsx │ │ ├── GrafanaLogo.tsx │ │ ├── LoadingMessage.tsx │ │ ├── Profile.tsx │ │ ├── ProfileState │ │ │ ├── ConfirmSignOut.tsx │ │ │ ├── Loading.tsx │ │ │ ├── SignedIn.tsx │ │ │ ├── SignedOut.tsx │ │ │ ├── SigningIn.tsx │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ └── index.ts │ ├── ProxyHealthWarning.tsx │ ├── ResponseStatusBadge.tsx │ ├── RunInCloudDialog │ │ ├── RunInCloudButton.tsx │ │ ├── RunInCloudContent.tsx │ │ ├── RunInCloudDialog.tsx │ │ └── states │ │ │ ├── Error.tsx │ │ │ ├── Loading.tsx │ │ │ ├── SignIn.tsx │ │ │ ├── index.tsx │ │ │ └── types.ts │ ├── SearchField.tsx │ ├── Settings │ │ ├── AccountSettings.tsx │ │ ├── AppearanceSettings.tsx │ │ ├── LogsSettings.tsx │ │ ├── ProxySettings.tsx │ │ ├── RecorderSettings.tsx │ │ ├── SettingsDialog.tsx │ │ ├── SettingsSection.tsx │ │ ├── TelemetrySettings.tsx │ │ ├── UpstreamProxySettings.tsx │ │ └── types.ts │ ├── StaticAssetsFilter.tsx │ ├── StyledReactSelect │ │ ├── StyledReactSelect.styles.ts │ │ ├── StyledReactSelect.tsx │ │ └── index.ts │ ├── Table.tsx │ ├── TableCellWithTooltip.tsx │ ├── TableSkeleton.tsx │ ├── TextButton.tsx │ ├── TextSpinner │ │ └── TextSpinner.tsx │ ├── TextWithTooltip.tsx │ ├── ThemeSwitcher.tsx │ ├── Toast │ │ ├── Toast.styles.ts │ │ ├── Toast.tsx │ │ └── Toasts.tsx │ ├── WebLogView │ │ ├── ContentPreview.tsx │ │ ├── Cookies.tsx │ │ ├── Details.hooks.ts │ │ ├── Details.tsx │ │ ├── Filter.hooks.ts │ │ ├── Filter.tsx │ │ ├── Group.tsx │ │ ├── RequestDetails │ │ │ ├── Headers.tsx │ │ │ ├── Payload.tsx │ │ │ ├── QueryParams.tsx │ │ │ ├── RequestDetails.tsx │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── ResponseDetails │ │ │ ├── Content.tsx │ │ │ ├── Font.tsx │ │ │ ├── Headers.tsx │ │ │ ├── Preview.tsx │ │ │ ├── Raw.tsx │ │ │ ├── ResponseDetails.tsx │ │ │ ├── ResponseDetails.utils.ts │ │ │ └── index.ts │ │ ├── Row.tsx │ │ ├── SearchResults.tsx │ │ ├── SearchResults.utils.ts │ │ ├── Tabs.tsx │ │ ├── WebLogView.tsx │ │ ├── WebLogView.utils.test.ts │ │ ├── WebLogView.utils.ts │ │ └── index.ts │ ├── icons │ │ ├── GeneratorIcon.tsx │ │ ├── GrafanaIcon.tsx │ │ ├── HomeIcon.tsx │ │ ├── PlusIcon.tsx │ │ ├── RecorderIcon.tsx │ │ ├── ValidatorIcon.tsx │ │ ├── WordWrapIcon.tsx │ │ └── index.ts │ └── primitives │ │ ├── Button.tsx │ │ ├── ContainerProvider.tsx │ │ ├── Flex.tsx │ │ ├── Input.tsx │ │ ├── Kbd.tsx │ │ ├── Label.tsx │ │ ├── Popover.tsx │ │ ├── RadioGroup.tsx │ │ ├── Table.tsx │ │ ├── Text.tsx │ │ ├── TextArea.tsx │ │ ├── TextField.tsx │ │ ├── Theme.tsx │ │ ├── Toolbar.tsx │ │ └── Tooltip.tsx ├── constants │ ├── files.ts │ ├── imports.ts │ ├── index.ts │ └── workspace.ts ├── electron.d.ts ├── globalStyles.ts ├── handlers │ ├── app │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── auth │ │ ├── fs.ts │ │ ├── index.ts │ │ ├── preload.ts │ │ ├── states.ts │ │ └── types.ts │ ├── browser │ │ ├── index.ts │ │ ├── launch.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── browserRemote │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── cloud │ │ ├── index.ts │ │ ├── preload.ts │ │ ├── states.ts │ │ └── types.ts │ ├── dataFiles │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── generator │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── har │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── index.ts │ ├── log │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── proxy │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── script │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── settings │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ ├── ui │ │ ├── index.ts │ │ ├── preload.ts │ │ └── types.ts │ └── utils.ts ├── hooks │ ├── useAutoScroll.test.ts │ ├── useAutoScroll.ts │ ├── useCloseSplashScreen.test.ts │ ├── useCloseSplashScreen.ts │ ├── useCreateGenerator.test.ts │ ├── useCreateGenerator.ts │ ├── useImportDataFile.ts │ ├── useListenBrowserEvent.ts │ ├── useListenProxyData.test.ts │ ├── useListenProxyData.ts │ ├── useOpenInDefaultApp.ts │ ├── useOverflowCheck.test.ts │ ├── useOverflowCheck.ts │ ├── useProxyDataGroups.test.ts │ ├── useProxyDataGroups.ts │ ├── useProxyHealthCheck.ts │ ├── useProxyStatus.ts │ ├── useRenameFile.ts │ ├── useRunChecks.test.ts │ ├── useRunChecks.ts │ ├── useRunLogs.test.ts │ ├── useRunLogs.ts │ ├── useScriptPreview.test.ts │ ├── useScriptPreview.ts │ ├── useSettings.ts │ ├── useTheme.test.ts │ └── useTheme.ts ├── index.tsx ├── main.ts ├── main │ ├── __snapshots__ │ │ └── script │ │ │ ├── browser-options │ │ │ ├── with-browser-import.js │ │ │ └── without-browser-import.js │ │ │ ├── checks-shim │ │ │ ├── handle-summary-was-exported.js │ │ │ └── handle-summary-was-not-exported.js │ │ │ ├── groups-shim │ │ │ ├── with-different-alias-for-execution-import.js │ │ │ ├── with-http-import.js │ │ │ └── without-http-import.js │ │ │ └── options-export │ │ │ ├── options-export.js │ │ │ ├── rename-complex-existing-options-export.js │ │ │ └── rename-existing-options-export.js │ ├── file.ts │ ├── generator.ts │ ├── healthCheck.ts │ ├── k6StudioState.ts │ ├── logger.ts │ ├── menu.ts │ ├── proxy.ts │ ├── script.test.ts │ ├── script.ts │ ├── settings.ts │ ├── watcher.ts │ └── window.ts ├── preload.ts ├── renderer.ts ├── routeMap.ts ├── rules │ ├── correlation.ts │ ├── correlation.utils.ts │ ├── customCode.ts │ ├── parameterization.test.ts │ ├── parameterization.ts │ ├── rules.ts │ ├── selectors │ │ ├── text.test.ts │ │ └── text.ts │ ├── shared.ts │ ├── utils.ts │ ├── verification.test.ts │ ├── verification.ts │ └── verification.utils.ts ├── schemas │ ├── exportScript.ts │ ├── generator │ │ ├── README.md │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── v0 │ │ │ ├── index.ts │ │ │ ├── rules.ts │ │ │ ├── testData.ts │ │ │ └── testOptions.ts │ │ ├── v1 │ │ │ ├── index.ts │ │ │ ├── loadZone.ts │ │ │ ├── rules.ts │ │ │ ├── testData.ts │ │ │ ├── testOptions.ts │ │ │ └── thresholds.ts │ │ └── v2 │ │ │ ├── index.ts │ │ │ ├── loadZone.ts │ │ │ ├── rules.ts │ │ │ ├── testData.ts │ │ │ ├── testOptions.ts │ │ │ └── thresholds.ts │ ├── imports.ts │ ├── profile │ │ ├── index.ts │ │ └── v1 │ │ │ └── index.ts │ ├── recording │ │ ├── index.ts │ │ └── v1 │ │ │ ├── browser.ts │ │ │ └── recording.ts │ └── settings │ │ ├── README.md │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── v1 │ │ └── index.ts │ │ ├── v2 │ │ └── index.ts │ │ └── v3 │ │ └── index.ts ├── sentry.ts ├── services │ ├── browser │ │ ├── schemas │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ └── server.ts │ │ └── server.ts │ ├── grafana │ │ ├── api.ts │ │ ├── authenticate.ts │ │ └── index.ts │ └── k6 │ │ ├── index.ts │ │ ├── projects.ts │ │ ├── schemas.ts │ │ ├── tests.ts │ │ ├── types.ts │ │ └── utils.ts ├── store │ ├── features │ │ ├── index.ts │ │ └── useFeaturesStore.ts │ ├── generator │ │ ├── fixtures.ts │ │ ├── hooks │ │ │ ├── useApplyRules.ts │ │ │ ├── useHighlightRequestChanges.ts │ │ │ └── useOriginalRequest.ts │ │ ├── index.ts │ │ ├── selectors.ts │ │ ├── slices │ │ │ ├── index.ts │ │ │ ├── recording.ts │ │ │ ├── recording.utils.ts │ │ │ ├── rules.ts │ │ │ ├── script.ts │ │ │ ├── testData.ts │ │ │ └── testOptions │ │ │ │ ├── index.ts │ │ │ │ ├── loadProfile.ts │ │ │ │ ├── loadZones.ts │ │ │ │ ├── thesholds.ts │ │ │ │ └── thinkTime.ts │ │ └── useGeneratorStore.ts │ └── ui │ │ ├── index.ts │ │ ├── useStudioUIStore.ts │ │ └── useToast.ts ├── test │ ├── factories │ │ ├── generator.ts │ │ ├── k6Check.ts │ │ ├── k6Log.ts │ │ ├── loadZones.ts │ │ ├── proxyData.ts │ │ └── threshold.ts │ ├── fixtures │ │ ├── checksRecording.ts │ │ ├── correlationRecording.ts │ │ └── parameterizationRules.ts │ ├── types.d.ts │ └── utils │ │ └── mockMatchMedia.ts ├── types │ ├── auth.ts │ ├── constrainedEditor.d.ts │ ├── features.ts │ ├── fuse.ts │ ├── generator.ts │ ├── har.ts │ ├── imports.ts │ ├── index.ts │ ├── rules.ts │ ├── settings.ts │ ├── testData.ts │ ├── testOptions.ts │ └── toast.ts ├── usageReport.ts ├── utils │ ├── async.ts │ ├── bugReport.ts │ ├── cloud.ts │ ├── dataFile.ts │ ├── diff.ts │ ├── electron.ts │ ├── errors.ts │ ├── file.ts │ ├── fileSystem.ts │ ├── form.ts │ ├── format.ts │ ├── fuse.ts │ ├── generator.ts │ ├── groups.ts │ ├── harToProxyData.ts │ ├── headers.ts │ ├── json.ts │ ├── k6Client.ts │ ├── operatorLabels.tsx │ ├── plist.ts │ ├── prettify.ts │ ├── proxyDataToHar.ts │ ├── query.ts │ ├── react.ts │ ├── rules.ts │ ├── serializers │ │ └── recording.ts │ ├── staticAssets.ts │ ├── thinkTime.ts │ ├── typescript.ts │ ├── uuid.ts │ └── workspace.ts ├── views │ ├── DataFile │ │ ├── DataFile.hooks.ts │ │ ├── DataFile.tsx │ │ ├── DataFileControls.tsx │ │ ├── DataFileTable.tsx │ │ └── index.ts │ ├── Generator │ │ ├── Allowlist │ │ │ ├── Allowlist.tsx │ │ │ ├── AllowlistCheckGroup.tsx │ │ │ ├── AllowlistDialog.tsx │ │ │ └── index.ts │ │ ├── ExportScriptDialog │ │ │ ├── ExportScriptDialog.tsx │ │ │ ├── ExportScriptDialog.utils.ts │ │ │ ├── OverwriteFileWarning.tsx │ │ │ ├── ScriptNameForm.tsx │ │ │ └── index.ts │ │ ├── Generator.hooks.ts │ │ ├── Generator.tsx │ │ ├── Generator.utils.ts │ │ ├── GeneratorControls │ │ │ ├── GeneratorControls.tsx │ │ │ └── index.ts │ │ ├── GeneratorTabs │ │ │ ├── GeneratorTabs.tsx │ │ │ ├── RequestList │ │ │ │ ├── Header.tsx │ │ │ │ ├── RequestList.tsx │ │ │ │ ├── RequestList.utils.tsx │ │ │ │ ├── RequestRow.tsx │ │ │ │ ├── RequestTable.tsx │ │ │ │ ├── RuleBadges.tsx │ │ │ │ └── index.ts │ │ │ ├── ScriptPreview.tsx │ │ │ ├── ScriptPreviewError.tsx │ │ │ └── index.ts │ │ ├── NewRuleMenu.tsx │ │ ├── RecordingSelector.tsx │ │ ├── RuleEditor │ │ │ ├── CorrelationEditor.tsx │ │ │ ├── CustomCodeEditor.tsx │ │ │ ├── FilterField.tsx │ │ │ ├── HeaderSelect.hooks.ts │ │ │ ├── HeaderSelect.tsx │ │ │ ├── ParameterizationEditor │ │ │ │ ├── CustomCode.tsx │ │ │ │ ├── FileSelect.tsx │ │ │ │ ├── ParameterizationEditor.tsx │ │ │ │ └── ValueEditor.tsx │ │ │ ├── RuleEditor.tsx │ │ │ ├── SelectorField.constants.ts │ │ │ ├── SelectorField.tsx │ │ │ ├── VariableSelect.tsx │ │ │ ├── VerificationEditor │ │ │ │ ├── ValueEditor.tsx │ │ │ │ ├── ValueEditor.utils.ts │ │ │ │ ├── VerificationEditor.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── TestData │ │ │ ├── DataFiles.tsx │ │ │ ├── TestData.tsx │ │ │ ├── VariablesEditor.tsx │ │ │ └── index.ts │ │ ├── TestOptions │ │ │ ├── LoadProfile │ │ │ │ ├── LoadProfile.tsx │ │ │ │ ├── components │ │ │ │ │ ├── Executor.tsx │ │ │ │ │ └── ExecutorOptions │ │ │ │ │ │ ├── ExecutorOptions.tsx │ │ │ │ │ │ ├── RampingVUs │ │ │ │ │ │ ├── RampingVUs.tsx │ │ │ │ │ │ ├── Stage.tsx │ │ │ │ │ │ ├── VUStages.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── SharedIterations │ │ │ │ │ │ ├── SharedIterations.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── LoadZones │ │ │ │ ├── LoadZoneRow.tsx │ │ │ │ ├── LoadZones.tsx │ │ │ │ ├── LoadZones.utils.tsx │ │ │ │ └── index.ts │ │ │ ├── TestOptions.tsx │ │ │ ├── ThinkTime.tsx │ │ │ ├── Thresholds │ │ │ │ ├── ThresholdRow.tsx │ │ │ │ ├── Thresholds.tsx │ │ │ │ ├── Thresholds.utils.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── TestRuleContainer │ │ │ ├── RulesNotAppliedCallout.tsx │ │ │ ├── SortableRuleList.tsx │ │ │ ├── StickyPanelHeader.tsx │ │ │ ├── TestRule │ │ │ │ ├── CustomCodeContent.tsx │ │ │ │ ├── TestRule.tsx │ │ │ │ ├── TestRuleActions.tsx │ │ │ │ ├── TestRuleFilter.tsx │ │ │ │ ├── TestRuleInlineContent.tsx │ │ │ │ ├── TestRuleSelector.tsx │ │ │ │ ├── TestRuleToggle.tsx │ │ │ │ ├── TestRuleTypeBadge.tsx │ │ │ │ ├── VerificationContent.tsx │ │ │ │ └── index.ts │ │ │ ├── TestRuleContainer.tsx │ │ │ └── index.ts │ │ ├── UnsavedChangesDialog.tsx │ │ └── ValidatorDialog.tsx │ ├── Home │ │ ├── Home.tsx │ │ ├── NavigationCard.tsx │ │ └── index.ts │ ├── Recorder │ │ ├── BrowserEventLog.tsx │ │ ├── ClearRequestsButton.tsx │ │ ├── ConfirmNavigationDialog.tsx │ │ ├── EmptyState.tsx │ │ ├── Recorder.tsx │ │ ├── Recorder.utils.ts │ │ ├── RecordingContext.tsx │ │ ├── RecordingInspector.tsx │ │ ├── RequestLog.tsx │ │ ├── RequestsSection.tsx │ │ ├── index.ts │ │ └── types.ts │ ├── RecordingPreviewer │ │ ├── RecordingPreviewer.tsx │ │ └── index.ts │ └── Validator │ │ ├── CheckRow.tsx │ │ ├── ChecksSection.tsx │ │ ├── ChecksSection.utils.test.ts │ │ ├── ChecksSection.utils.ts │ │ ├── LogsSection.tsx │ │ ├── Validator.hooks.ts │ │ ├── Validator.tsx │ │ ├── ValidatorContent.tsx │ │ ├── ValidatorControls.tsx │ │ ├── ValidatorEmptyState.tsx │ │ └── index.ts └── vite-env.d.ts ├── tsconfig.json ├── update_version.py ├── vite.base.config.ts ├── vite.extension.config.mts ├── vite.main.config.ts ├── vite.preload.config.ts ├── vite.renderer.config.ts └── vitest.config.ts /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Use this template for reporting bugs. Please search existing issues first. 3 | labels: needs-triage 4 | type: Bug 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Describe the bug 9 | - type: textarea 10 | attributes: 11 | label: Steps to reproduce the problem 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Expected behavior 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: Actual behavior 22 | validations: 23 | required: true 24 | - type: input 25 | id: version 26 | attributes: 27 | label: Grafana k6 Studio version 28 | validations: 29 | required: true 30 | - type: input 31 | id: os 32 | attributes: 33 | label: OS 34 | description: e.g. Windows 11, Arch Linux, macOS 15.3.1, etc. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Use this template for requesting new features. Please search existing issues first. 3 | labels: needs-triage 4 | type: Feature 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Feature description 9 | description: A clear and concise description of the problem or missing capability 10 | - type: textarea 11 | attributes: 12 | label: Suggested solution (optional) 13 | description: If you have a solution in mind, please describe it. 14 | - type: textarea 15 | attributes: 16 | label: Already existing or connected issues / PRs (optional) 17 | description: If you have found some issues or pull requests that are related to your new issue, please link them here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/project_spec.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Project spec 3 | about: Project specification 4 | title: '' 5 | assignees: '' 6 | --- 7 | 8 | ## Description 9 | 10 | 11 | 12 | ## Considerations 13 | 14 | 15 | 16 | ## Acceptance criteria 17 | 18 | 19 | 20 | - ... 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## How to Test 8 | 9 | 10 | 11 | ## Checklist 12 | 13 | - [ ] I have performed a self-review of my code. 14 | - [ ] I have added tests for my changes. 15 | - [ ] I have run linter locally (`npm run lint`) and all checks pass. 16 | - [ ] I have run tests locally (`npm test`) and all tests pass. 17 | - [ ] I have commented on my code, particularly in hard-to-understand areas. 18 | 19 | 20 | ## Screenshots (if appropriate): 21 | 22 | ## Related PR(s)/Issue(s) 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | build: 12 | permissions: 13 | contents: read 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | persist-credentials: false 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | cache: 'npm' 23 | - run: npm ci 24 | - run: npm run lint 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.github/workflows/commit-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR title 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | pull-requests: read 12 | 13 | jobs: 14 | main: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | types: | 23 | fix 24 | feat 25 | refactor 26 | style 27 | test 28 | perf 29 | docs 30 | deps 31 | build 32 | ci 33 | chore 34 | internal 35 | revert 36 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | release-please: 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 18 | with: 19 | token: ${{ secrets.RELEASE_PLEASE_TOKEN}} 20 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/__snapshots__/**/*.ts 2 | **/__snapshots__/**/*.js 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "trailingComma": "es5", 4 | "jsxSingleQuote": false, 5 | "singleQuote": true, 6 | "useTabs": false, 7 | "tabWidth": 2, 8 | "semi": false, 9 | "printWidth": 80 10 | } 11 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.3.0" 3 | } 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @grafana/k6-Studio 2 | -------------------------------------------------------------------------------- /assets/k6-studio-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/assets/k6-studio-screenshot.png -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: k6-studio 5 | title: k6-studio 6 | description: | 7 | Desktop application for Mac, Windows and Linux designed to help you generate k6 test scripts 8 | annotations: 9 | github.com/project-slug: grafana/k6-studio 10 | spec: 11 | type: tool 12 | owner: group:default/k6-studio 13 | lifecycle: production 14 | -------------------------------------------------------------------------------- /entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.files.user-selected.read-write 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.network.server 14 | 15 | com.apple.security.cs.disable-library-validation 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /extension/src/background/routing.ts: -------------------------------------------------------------------------------- 1 | import { BrowserExtensionClient } from '../messaging' 2 | import { BufferedTransport } from '../messaging/transports/buffered' 3 | import { NullTransport } from '../messaging/transports/null' 4 | import { PortTransport } from '../messaging/transports/port' 5 | import { WebSocketTransport } from '../messaging/transports/webSocket' 6 | 7 | const background = new BrowserExtensionClient('background') 8 | 9 | const frontend = new BrowserExtensionClient('frontend', new PortTransport()) 10 | 11 | const studio = new BrowserExtensionClient( 12 | 'studio', 13 | STANDALONE_EXTENSION 14 | ? new NullTransport() 15 | : new BufferedTransport(new WebSocketTransport('ws://localhost:7554')) 16 | ) 17 | 18 | background.forward('events-recorded', [studio, frontend]) 19 | background.forward('events-loaded', [frontend]) 20 | 21 | studio.forward('navigate', [background]) 22 | studio.forward('highlight-elements', [frontend]) 23 | 24 | frontend.forward('record-events', [background]) 25 | frontend.forward('navigate', [background]) 26 | frontend.forward('load-events', [background]) 27 | frontend.forward('stop-recording', [studio]) 28 | 29 | export { background as client } 30 | -------------------------------------------------------------------------------- /extension/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Flag to determine if the extension has been built as part of k6 Studio 3 | * or if it's built as a standalone extension. This is used to e.g. determine 4 | * if we should open a websocket connection to k6 Studio. 5 | */ 6 | declare const STANDALONE_EXTENSION: boolean 7 | -------------------------------------------------------------------------------- /extension/src/frontend/routing.ts: -------------------------------------------------------------------------------- 1 | import { BrowserExtensionClient } from '../messaging' 2 | import { BackgroundTransport } from '../messaging/transports/background' 3 | 4 | const frontend = new BrowserExtensionClient('frontend') 5 | 6 | const background = new BrowserExtensionClient( 7 | 'recorder', 8 | new BackgroundTransport('recorder') 9 | ) 10 | 11 | frontend.forward('record-events', [background]) 12 | frontend.forward('navigate', [background]) 13 | frontend.forward('load-events', [background]) 14 | frontend.forward('stop-recording', [background]) 15 | 16 | background.forward('events-recorded', [frontend]) 17 | background.forward('events-loaded', [frontend]) 18 | background.forward('highlight-elements', [frontend]) 19 | 20 | export { frontend as client } 21 | -------------------------------------------------------------------------------- /extension/src/frontend/view/Anchor.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react' 2 | import { forwardRef } from 'react' 3 | 4 | import { Position } from './types' 5 | 6 | interface AnchorProps { 7 | position: Position 8 | } 9 | 10 | export const Anchor = forwardRef(function Anchor( 11 | { position }, 12 | ref 13 | ) { 14 | return ( 15 |
25 | ) 26 | }) 27 | -------------------------------------------------------------------------------- /extension/src/frontend/view/ElementInspector/ElementInspector.utils.ts: -------------------------------------------------------------------------------- 1 | import { Assertion } from '@/schemas/recording' 2 | import { exhaustive } from '@/utils/typescript' 3 | 4 | import { AssertionData } from './assertions/types' 5 | 6 | export function toAssertion(data: AssertionData): Assertion { 7 | switch (data.type) { 8 | case 'visibility': 9 | return { 10 | type: 'visibility', 11 | visible: data.state === 'visible', 12 | } 13 | 14 | case 'text': 15 | return { 16 | type: 'text', 17 | operation: { 18 | type: 'contains', 19 | value: data.text, 20 | }, 21 | } 22 | 23 | default: 24 | return exhaustive(data) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /extension/src/frontend/view/ElementInspector/assertions/types.ts: -------------------------------------------------------------------------------- 1 | export interface VisibilityAssertionData { 2 | type: 'visibility' 3 | selector: string 4 | state: 'visible' | 'hidden' 5 | } 6 | 7 | export interface TextAssertionData { 8 | type: 'text' 9 | selector: string 10 | text: string 11 | } 12 | 13 | export type AssertionData = VisibilityAssertionData | TextAssertionData 14 | -------------------------------------------------------------------------------- /extension/src/frontend/view/ElementInspector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ElementInspector' 2 | -------------------------------------------------------------------------------- /extension/src/frontend/view/GlobalStyles.tsx: -------------------------------------------------------------------------------- 1 | import { css, Global } from '@emotion/react' 2 | import { useEffect } from 'react' 3 | 4 | const uuid = crypto.randomUUID().replace(/-/g, '').slice(0, 8) 5 | 6 | type GlobalClass = 'inspecting' | 'asserting-text' 7 | 8 | export function useGlobalClass(name: GlobalClass) { 9 | useEffect(() => { 10 | const className = `ksix-studio-${name}-${uuid}` 11 | 12 | document.body.classList.add(className) 13 | 14 | return () => { 15 | document.body.classList.remove(className) 16 | } 17 | }, [name]) 18 | } 19 | 20 | export function GlobalStyles() { 21 | return ( 22 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /extension/src/frontend/view/Overlay.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react' 2 | import { ComponentProps, forwardRef } from 'react' 3 | 4 | import { Bounds } from './types' 5 | 6 | interface OverlayProps extends ComponentProps<'div'> { 7 | bounds: Bounds 8 | } 9 | 10 | export const Overlay = forwardRef( 11 | function Overlay({ bounds, ...props }, ref) { 12 | return ( 13 |
24 | ) 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /extension/src/frontend/view/TextSelectionPopover.types.ts: -------------------------------------------------------------------------------- 1 | import { ElementSelector } from '@/schemas/recording' 2 | 3 | import { Bounds } from './types' 4 | 5 | export interface TextSelection { 6 | text: string 7 | selector: ElementSelector 8 | range: Range 9 | bounds: Bounds 10 | highlights: Bounds[] 11 | } 12 | -------------------------------------------------------------------------------- /extension/src/frontend/view/hooks/useEscape.tsx: -------------------------------------------------------------------------------- 1 | import { DependencyList } from 'react' 2 | import useKey from 'react-use/lib/useKey' 3 | 4 | export function useEscape(callback: () => void, dependencies?: DependencyList) { 5 | useKey('Escape', callback, {}, dependencies) 6 | } 7 | -------------------------------------------------------------------------------- /extension/src/frontend/view/hooks/useHighlightDebounce.tsx: -------------------------------------------------------------------------------- 1 | import { useDebouncedValue } from './useDebouncedValue' 2 | 3 | export function useHighlightDebounce(value: T) { 4 | // We add a very slight debounce to prevent the worst of the 5 | // flickering when moving the mouse over a page. 6 | return useDebouncedValue({ 7 | value, 8 | delay: 30, 9 | maxWait: 60, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /extension/src/frontend/view/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | import { Tool } from './types' 4 | 5 | interface InBrowserUIStore { 6 | tool: Tool | null 7 | selectTool: (tool: Tool | null) => void 8 | } 9 | 10 | export const useInBrowserUIStore = create((set) => { 11 | return { 12 | tool: null, 13 | selectTool: (tool: Tool | null) => set({ tool }), 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /extension/src/frontend/view/types.ts: -------------------------------------------------------------------------------- 1 | export interface Position { 2 | top: number 3 | left: number 4 | } 5 | 6 | export interface Bounds extends Position { 7 | width: number 8 | height: number 9 | } 10 | 11 | export type Tool = 'inspect' | 'assert-text' 12 | -------------------------------------------------------------------------------- /extension/src/messaging/transports/null.ts: -------------------------------------------------------------------------------- 1 | import { Transport } from './transport' 2 | 3 | /** 4 | * Transport that does nothing. Useful if you only 5 | * want to send messages to yourself. 6 | */ 7 | export class NullTransport extends Transport { 8 | send(): void {} 9 | } 10 | -------------------------------------------------------------------------------- /extension/src/messaging/transports/port.ts: -------------------------------------------------------------------------------- 1 | import { runtime, Runtime } from 'webextension-polyfill' 2 | 3 | import { Transport } from './transport' 4 | 5 | /** 6 | * Maintains connections to one or more ports, e.g. from 7 | * content scripts. 8 | */ 9 | export class PortTransport extends Transport { 10 | #ports: Array = [] 11 | 12 | constructor() { 13 | super() 14 | 15 | runtime.onConnect.addListener((port) => { 16 | this.#ports.push(port) 17 | 18 | port.onMessage.addListener((message) => { 19 | this.emit('message', { 20 | sender: { 21 | tab: port.sender?.tab?.id?.toString() ?? null, 22 | }, 23 | data: message, 24 | }) 25 | }) 26 | 27 | port.onDisconnect.addListener(() => { 28 | this.#ports = this.#ports.filter((p) => p !== port) 29 | }) 30 | }) 31 | } 32 | 33 | send(data: unknown): void { 34 | for (const port of this.#ports) { 35 | port.postMessage(data) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /extension/src/messaging/transports/transport.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { EventEmitter } from 'extension/src/utils/events' 4 | 5 | interface TransportMessages { 6 | connect: void 7 | disconnect: void 8 | message: { 9 | sender?: Sender 10 | data: unknown 11 | } 12 | } 13 | 14 | export const SenderSchema = z.object({ 15 | tab: z.string().nullable(), 16 | }) 17 | 18 | export type Sender = z.infer 19 | 20 | /** 21 | * A generic transport that can do two things: 22 | * 23 | * * Send data over some channel (e.g. a web socket, a message port, etc.) 24 | * * Receive messages from the channel (via the "message" event)`) 25 | * 26 | * This abstraction lets us send data without caring about how it's being sent. 27 | */ 28 | export abstract class Transport extends EventEmitter { 29 | get connected() { 30 | return true 31 | } 32 | 33 | abstract send(data: unknown, sender?: Sender): void 34 | 35 | dispose() {} 36 | } 37 | -------------------------------------------------------------------------------- /extension/src/messaging/transports/webSocketServer.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketServer } from 'ws' 2 | 3 | import { Transport } from './transport' 4 | 5 | export class WebSocketServerTransport extends Transport { 6 | #server: WebSocketServer 7 | 8 | constructor(host: string, port: number) { 9 | super() 10 | 11 | this.#server = new WebSocketServer({ host, port }) 12 | 13 | this.#server.on('connection', (socket) => { 14 | socket.on('message', (data) => { 15 | const chunks = Array.isArray(data) ? data : [data] 16 | const decoder = new TextDecoder() 17 | 18 | const message = chunks.reduce((acc, chunk) => { 19 | return acc + decoder.decode(chunk) 20 | }, '') 21 | 22 | this.emit('message', { 23 | sender: undefined, 24 | data: JSON.parse(message), 25 | }) 26 | }) 27 | }) 28 | } 29 | 30 | send(data: unknown): void { 31 | const message = JSON.stringify(data) 32 | 33 | this.#server.clients.forEach((client) => { 34 | client.send(message) 35 | }) 36 | } 37 | 38 | dispose() { 39 | this.#server.close() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /extension/src/selectors.ts: -------------------------------------------------------------------------------- 1 | import { finder } from '@medv/finder' 2 | 3 | import { ElementSelector } from '@/schemas/recording' 4 | 5 | export function generateSelector(element: Element): ElementSelector { 6 | return { 7 | css: finder(element, {}), 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | Grafana k6 Studio 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/checks_snippet.js: -------------------------------------------------------------------------------- 1 | export function handleSummary(data) { 2 | const checks = [] 3 | 4 | function traverseGroup(group) { 5 | if (group.checks) { 6 | group.checks.forEach((check) => { 7 | checks.push(check) 8 | }) 9 | } 10 | if (group.groups) { 11 | group.groups.forEach((subGroup) => { 12 | traverseGroup(subGroup) 13 | }) 14 | } 15 | } 16 | 17 | data.root_group.checks.forEach((check) => { 18 | checks.push(check) 19 | }) 20 | data.root_group.groups.forEach((group) => { 21 | traverseGroup(group) 22 | }) 23 | 24 | return { 25 | stdout: JSON.stringify(checks), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/icons/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/resources/icons/logo.icns -------------------------------------------------------------------------------- /resources/icons/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/resources/icons/logo.ico -------------------------------------------------------------------------------- /resources/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/resources/icons/logo.png -------------------------------------------------------------------------------- /resources/linux/arm64/k6-studio-proxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/resources/linux/arm64/k6-studio-proxy -------------------------------------------------------------------------------- /resources/linux/x86_64/k6-studio-proxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/resources/linux/x86_64/k6-studio-proxy -------------------------------------------------------------------------------- /resources/mac/arm64/k6-studio-proxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/resources/mac/arm64/k6-studio-proxy -------------------------------------------------------------------------------- /resources/mac/x86_64/k6-studio-proxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/resources/mac/x86_64/k6-studio-proxy -------------------------------------------------------------------------------- /resources/win/x86_64/k6-studio-proxy.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/resources/win/x86_64/k6-studio-proxy.exe -------------------------------------------------------------------------------- /src/assets/fonts/Inter/InterVariable.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/k6-studio/83ff7eb80ec86f8ddb1b7eab5cd7286e6399d4b0/src/assets/fonts/Inter/InterVariable.woff2 -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/assertions/element-contains-text.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | import { expect } from "https://jslib.k6.io/k6-testing/0.4.0/index.js"; 5 | 6 | export const options = { 7 | scenarios: { 8 | default: { 9 | executor: "shared-iterations", 10 | options: { browser: { type: "chromium" } }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default async function () { 16 | const page = await browser.newPage(); 17 | 18 | await expect(page.locator("button")).toContainText("Hello, World!"); 19 | } 20 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/assertions/element-is-hidden.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | import { expect } from "https://jslib.k6.io/k6-testing/0.4.0/index.js"; 5 | 6 | export const options = { 7 | scenarios: { 8 | default: { 9 | executor: "shared-iterations", 10 | options: { browser: { type: "chromium" } }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default async function () { 16 | const page = await browser.newPage(); 17 | 18 | await expect(page.locator("button")).toBeHidden(); 19 | } 20 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/assertions/element-is-visible.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | import { expect } from "https://jslib.k6.io/k6-testing/0.4.0/index.js"; 5 | 6 | export const options = { 7 | scenarios: { 8 | default: { 9 | executor: "shared-iterations", 10 | options: { browser: { type: "chromium" } }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default async function () { 16 | const page = await browser.newPage(); 17 | 18 | await expect(page.locator("button")).toBeVisible(); 19 | } 20 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/check-element.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page.locator('input[type="checkbox"]').check(); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/click-element-with-modifier-keys.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page 18 | .locator("button") 19 | .click({ button: "right", modifiers: ["Control", "Shift", "Alt", "Meta"] }); 20 | } 21 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/click-element.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page.locator("button").click(); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/empty-browser-test.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | export const options = { 4 | scenarios: { default: { executor: "shared-iterations" } }, 5 | }; 6 | 7 | export default function () {} 8 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/goto-url.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page.goto("https://example.com"); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/reload-page.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page.reload(); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/right-click-element.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page.locator("button").click({ button: "right" }); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/select-multiple-options-on-element.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page.locator("select").selectOption(["option1", "option2"]); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/select-single-option-on-element.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page.locator("select").selectOption("option1"); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/type-text-on-element.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page.locator("input").type("Hello, World!"); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/browser/__snapshots__/browser/uncheck-element.ts: -------------------------------------------------------------------------------- 1 | // Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z 2 | 3 | import { browser } from "k6/browser"; 4 | 5 | export const options = { 6 | scenarios: { 7 | default: { 8 | executor: "shared-iterations", 9 | options: { browser: { type: "chromium" } }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default async function () { 15 | const page = await browser.newPage(); 16 | 17 | await page.locator('input[type="checkbox"]').check(); 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/browser/code/comment.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree as ts } from '@typescript-eslint/types' 2 | 3 | import { baseProps } from '@/codegen/estree/nodes' 4 | 5 | export function comment(value: string): ts.BlockStatement { 6 | // The type of node here is not important since it's just being used as 7 | // a placeholder for the comment. A BlockStatement just happens to be one 8 | // of the simplest nodes to build. 9 | return { 10 | ...baseProps, 11 | type: AST_NODE_TYPES.BlockStatement, 12 | comment: value, 13 | body: [], 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/codegen/browser/codegen.ts: -------------------------------------------------------------------------------- 1 | import { toTypeScriptAst } from './code' 2 | import { format } from './formatting/formatter' 3 | import { toIntermediateAst } from './intermediate' 4 | import { Test } from './types' 5 | 6 | export function emitScript(test: Test): Promise { 7 | const intermediate = toIntermediateAst(test) 8 | const ast = toTypeScriptAst(intermediate) 9 | 10 | return format(ast) 11 | } 12 | -------------------------------------------------------------------------------- /src/codegen/browser/index.ts: -------------------------------------------------------------------------------- 1 | export { emitScript } from './codegen' 2 | -------------------------------------------------------------------------------- /src/codegen/estree/index.ts: -------------------------------------------------------------------------------- 1 | import { TSESTree as ts } from '@typescript-eslint/types' 2 | 3 | import { baseProps, NodeType } from './nodes' 4 | import { NodeOptions } from './types' 5 | 6 | export function program({ 7 | body, 8 | comments = [], 9 | sourceType = 'module', 10 | tokens, 11 | }: NodeOptions): ts.Program { 12 | return { 13 | ...baseProps, 14 | type: NodeType.Program, 15 | body, 16 | comments, 17 | sourceType, 18 | tokens, 19 | } 20 | } 21 | 22 | export * from './modules' 23 | export * from './declarations' 24 | export * from './statements' 25 | export * from './expressions' 26 | -------------------------------------------------------------------------------- /src/codegen/estree/nodes.ts: -------------------------------------------------------------------------------- 1 | import { TSESTree as ts } from '@typescript-eslint/types' 2 | 3 | // Since we're generating our own AST we don't have any positional information, but 4 | // the types require it. To fix it, we spread this dummy object into the nodes we create. 5 | export const baseProps = { 6 | loc: { 7 | start: { line: 0, column: 0 }, 8 | end: { line: 0, column: 0 }, 9 | }, 10 | range: [0, 1] as [number, number], 11 | // The type definitions require us to set a parent, but we don't have one so we force 12 | // it to null. Prettier seems to be totally fine with this. The `any` is to work around 13 | // some types narrowing the parent type to a specific node type, e.g. the parent of an 14 | // `ImportSpecifier` must be an `ImportDeclaration`. 15 | // 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment 17 | parent: null as unknown as any, 18 | } 19 | 20 | export const NodeType = ts.AST_NODE_TYPES 21 | -------------------------------------------------------------------------------- /src/codegen/estree/statements.ts: -------------------------------------------------------------------------------- 1 | import { TSESTree as ts } from '@typescript-eslint/types' 2 | 3 | import { baseProps, NodeType } from './nodes' 4 | import { NodeOptions } from './types' 5 | 6 | export function block(body: ts.Statement[]): ts.BlockStatement { 7 | return { 8 | ...baseProps, 9 | type: NodeType.BlockStatement, 10 | body, 11 | } 12 | } 13 | 14 | export function expressionStatement({ 15 | expression, 16 | directive, 17 | }: NodeOptions): ts.ExpressionStatement { 18 | return { 19 | ...baseProps, 20 | type: NodeType.ExpressionStatement, 21 | expression, 22 | directive, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/codegen/estree/types.ts: -------------------------------------------------------------------------------- 1 | import { TSESTree as ts } from '@typescript-eslint/types' 2 | 3 | /** 4 | * This type makes it easier to create helper functions for creating nodes. It makes 5 | * every property in the node optional except for the specified required properties. 6 | * This lets us define sane defaults for rarely used properties. 7 | */ 8 | export type NodeOptions< 9 | T extends ts.Node | ts.Property, 10 | RequiredProps extends keyof T, 11 | OmitProps extends keyof T = never, 12 | > = Partial> & 13 | Pick 14 | 15 | export type LiteralOrExpression = 16 | | ts.Expression 17 | | string 18 | | number 19 | | boolean 20 | | null 21 | -------------------------------------------------------------------------------- /src/codegen/estree/typescript-estree.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@typescript-eslint/types' { 2 | namespace TSESTree { 3 | type NewLine = 'before' | 'after' | 'both' 4 | 5 | interface NodeOrTokenData { 6 | /** 7 | * If set, this node is a placeholder for a comment and a 8 | * comment with the value will be printed instead. 9 | */ 10 | comment?: string 11 | 12 | /** 13 | * Specifies how newlines should be added around the node. 14 | */ 15 | newLine?: NewLine 16 | } 17 | } 18 | } 19 | 20 | export {} 21 | -------------------------------------------------------------------------------- /src/codegen/imports.ts: -------------------------------------------------------------------------------- 1 | import { ImportModule } from '@/types/imports' 2 | import { exhaustive } from '@/utils/typescript' 3 | 4 | export function generateImportStatement(importModule: ImportModule): string { 5 | const imports: string[] = [] 6 | 7 | if (importModule.default) { 8 | imports.push(`${importModule.default.name}`) 9 | } 10 | 11 | if (importModule.imports) { 12 | switch (importModule.imports.type) { 13 | case 'named': 14 | imports.push( 15 | `{ ${importModule.imports.imports 16 | .map((i) => (i.alias ? `${i.name} as ${i.alias}` : i.name)) 17 | .join(', ')} }` 18 | ) 19 | break 20 | case 'namespace': 21 | imports.push(`* as ${importModule.imports.alias}`) 22 | break 23 | default: 24 | exhaustive(importModule.imports) 25 | } 26 | } 27 | 28 | // TODO: check if k6 supports side effect imports 29 | if (imports.length === 0) { 30 | return `import '${importModule.path}'` 31 | } 32 | 33 | return `import ${imports.join(', ')} from '${importModule.path}'` 34 | } 35 | -------------------------------------------------------------------------------- /src/codegen/index.ts: -------------------------------------------------------------------------------- 1 | export { generateScript } from './codegen' 2 | -------------------------------------------------------------------------------- /src/components/BrowserEventList/EventDescription/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EventDescription' 2 | -------------------------------------------------------------------------------- /src/components/BrowserEventList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BrowserEventList' 2 | -------------------------------------------------------------------------------- /src/components/ButtonWithTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps, Tooltip } from '@radix-ui/themes' 2 | import { forwardRef } from 'react' 3 | 4 | export const ButtonWithTooltip = forwardRef< 5 | HTMLButtonElement, 6 | ButtonProps & { tooltip: string } 7 | >(function ButtonWithTooltip({ tooltip, ...props }, ref) { 8 | return ( 9 |