mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-26 17:09:11 -06:00
feat: have a copy button on firefox (#20491)
This commit is contained in:
committed by
GitHub
parent
284ae6df6f
commit
315d704f89
@@ -1,5 +1,6 @@
|
||||
import type { Interception } from '@packages/net-stubbing/lib/external-types'
|
||||
import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json'
|
||||
import type { SinonStub } from 'sinon'
|
||||
|
||||
describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
beforeEach(() => {
|
||||
@@ -261,9 +262,10 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
cy.contains('--record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
})
|
||||
|
||||
it('displays a copy button', { browser: 'electron' }, () => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
it('displays a copy button', () => {
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {projectId: \'abcdef\'}')
|
||||
ctx.electronApi.copyTextToClipboard = o.sinon.stub()
|
||||
})
|
||||
|
||||
cy.loginUser()
|
||||
@@ -283,6 +285,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
cy.get('[href="#/runs"]').click()
|
||||
cy.get('[data-cy="copy-button"]').click()
|
||||
cy.contains('Copied!')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('cypress run --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -42,11 +42,20 @@ describe('App: Settings', () => {
|
||||
|
||||
describe('Cloud Settings', () => {
|
||||
it('shows the projectId section when there is a projectId', () => {
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
ctx.electronApi.copyTextToClipboard = o.sinon.stub()
|
||||
})
|
||||
|
||||
cy.startAppServer('e2e')
|
||||
cy.visitApp()
|
||||
cy.findByText('Settings').click()
|
||||
cy.findByText('Dashboard Settings').click()
|
||||
cy.findByText('Project ID').should('be.visible')
|
||||
cy.findByText('Copy').click()
|
||||
cy.findByText('Copied!').should('be.visible')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('abc123')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the Record Keys section', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
class="border-t border-b bg-gray-50 border-gray-200 h-56px grid py-12px px-16px gap-12px grid-cols-[40px,1fr,auto] items-center "
|
||||
>
|
||||
<button
|
||||
class="flex items-center justify-center h-full text-white transition duration-150 border rounded-md outline-none w-40px hover:default-ring"
|
||||
class="border rounded-md flex h-full outline-none text-white transition w-40px duration-150 items-center justify-center hover:default-ring"
|
||||
:class="[ selectorPlaygroundStore.isEnabled ? 'default-ring' : 'border-gray-200']"
|
||||
data-cy="playground-toggle"
|
||||
@click="toggleEnabled"
|
||||
@@ -12,22 +12,22 @@
|
||||
<i-cy-selector_x16 :class="{ 'icon-dark-indigo-500': selectorPlaygroundStore.isEnabled, 'icon-dark-gray-500': !selectorPlaygroundStore.isEnabled }" />
|
||||
</button>
|
||||
<div
|
||||
class="relative flex items-center flex-1 w-full h-full"
|
||||
class="flex h-full flex-1 w-full relative items-center"
|
||||
@mouseover="setShowingHighlight"
|
||||
>
|
||||
<Menu #="{ open }">
|
||||
<MenuButton
|
||||
:aria-label="t('runner.selectorPlayground.selectorMethodsLabel')"
|
||||
class="flex items-center justify-center h-full text-white border border-gray-200 outline-none rounded-l-md w-40px hocus-default border-r-transparent"
|
||||
class="border border-r-transparent rounded-l-md flex h-full outline-none border-gray-200 text-white w-40px items-center justify-center hocus-default"
|
||||
@click.stop
|
||||
>
|
||||
<i-cy-chevron-down-small_x16
|
||||
class="transition duration-300 transition-color"
|
||||
class="transition transition-color duration-300"
|
||||
:class="open ? 'icon-dark-indigo-500' : 'icon-dark-gray-500'"
|
||||
/>
|
||||
</MenuButton>
|
||||
<MenuItems
|
||||
class="absolute z-40 flex flex-col overflow-scroll text-white bg-gray-900 rounded outline-transparent top-34px"
|
||||
class="rounded flex flex-col outline-transparent bg-gray-900 text-white top-34px z-40 absolute overflow-scroll"
|
||||
>
|
||||
<MenuItem
|
||||
v-for="method in methods"
|
||||
@@ -36,7 +36,7 @@
|
||||
>
|
||||
<button
|
||||
:class="{ 'bg-gray-700': active }"
|
||||
class="text-left border-b border-b-gray-800 py-8px px-16px"
|
||||
class="border-b border-b-gray-800 text-left py-8px px-16px"
|
||||
@click="selectorPlaygroundStore.setMethod(method.value)"
|
||||
>
|
||||
{{ method.display }}
|
||||
@@ -44,10 +44,10 @@
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
<code class="relative flex-1 h-full">
|
||||
<code class="h-full flex-1 relative">
|
||||
<span
|
||||
ref="ghostLeft"
|
||||
class="absolute inset-y-0 flex items-center text-gray-600 pointer-events-none pl-12px"
|
||||
class="flex pl-12px inset-y-0 text-gray-600 absolute items-center pointer-events-none"
|
||||
data-cy="selected-playground-method"
|
||||
>
|
||||
<span class="text-gray-800">cy</span>.<span class="text-purple-500">{{ selectorPlaygroundStore.method }}</span>(‘
|
||||
@@ -57,7 +57,7 @@
|
||||
class="font-medium left-[-9999px] absolute inline-block"
|
||||
>{{ selector.replace(/\s/g, ' ') }}</span>
|
||||
<span
|
||||
class="absolute inset-y-0 flex items-center text-gray-600 pointer-events-none"
|
||||
class="flex inset-y-0 text-gray-600 absolute items-center pointer-events-none"
|
||||
:style="{left: inputRightOffset + 'px'}"
|
||||
>‘)</span>
|
||||
<input
|
||||
@@ -65,12 +65,12 @@
|
||||
v-model="selector"
|
||||
data-cy="playground-selector"
|
||||
:style="{paddingLeft: inputLeftOffset + 'px', paddingRight: matcherWidth + 32 + 24 + 'px'}"
|
||||
class="w-full h-full font-medium text-indigo-500 border border-gray-200 outline-none rounded-r-md hocus-default overflow-ellipsis"
|
||||
class="border rounded-r-md font-medium h-full outline-none border-gray-200 w-full text-indigo-500 hocus-default overflow-ellipsis"
|
||||
:class="{'hocus-default': selectorPlaygroundStore.isValid, 'hocus-error': !selectorPlaygroundStore.isValid}"
|
||||
>
|
||||
<div
|
||||
ref="match"
|
||||
class="absolute inset-y-0 right-0 flex items-center font-sans text-gray-600 border-l border-l-gray-200 my-6px px-16px"
|
||||
class="border-l flex font-sans border-l-gray-200 my-6px px-16px inset-y-0 right-0 text-gray-600 absolute items-center"
|
||||
data-cy="playground-num-elements"
|
||||
>
|
||||
<template v-if="!selectorPlaygroundStore.isValid">
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-12px">
|
||||
<SelectorPlaygroundTooltip v-if="isSupported">
|
||||
<SelectorPlaygroundTooltip>
|
||||
<Button
|
||||
size="md"
|
||||
variant="outline"
|
||||
@@ -134,9 +134,10 @@ import type { AutIframe } from '../aut-iframe'
|
||||
import type { EventManager } from '../event-manager'
|
||||
import Button from '@packages/frontend-shared/src/components/Button.vue'
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
|
||||
import { useClipboard, useElementSize } from '@vueuse/core'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import SelectorPlaygroundTooltip from './SelectorPlaygroundTooltip.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useClipboard } from '@cy/gql-components/useClipboard'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -218,7 +219,7 @@ function printSelected () {
|
||||
props.getAutIframe().printSelectorElementsToConsole()
|
||||
}
|
||||
|
||||
const { copy, isSupported } = useClipboard({ copiedDuring: 2000 })
|
||||
const { copy } = useClipboard({ copiedDuring: 2000 })
|
||||
const copyToClipboard = () => {
|
||||
copy(selector.value)
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql } from '@urql/core'
|
||||
import CopyButton from '@cy/components/CopyButton.vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import CopyButton from '@cy/gql-components/CopyButton.vue'
|
||||
import IconOctothorpe from '~icons/cy/octothorpe_x16.svg'
|
||||
import SettingsSection from '../SettingsSection.vue'
|
||||
import ExternalLink from '@cy/gql-components/ExternalLink.vue'
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { gql } from '@urql/core'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import CopyButton from '@cy/components/CopyButton.vue'
|
||||
import CopyButton from '@cy/gql-components/CopyButton.vue'
|
||||
import ExternalLink from '@cy/gql-components/ExternalLink.vue'
|
||||
import { useExternalLink } from '@cy/gql-components/useExternalLink'
|
||||
import IconKey from '~icons/cy/placeholder_x16.svg'
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface ElectronApiShape {
|
||||
showItemInFolder(folder: string): void
|
||||
showOpenDialog(props: OpenDialogOptions): Promise<OpenDialogReturnValue>
|
||||
showSaveDialog(window: BrowserWindow, props: SaveDialogOptions): Promise<SaveDialogReturnValue>
|
||||
copyTextToClipboard(text: string): void
|
||||
}
|
||||
|
||||
export class ElectronActions {
|
||||
|
||||
@@ -404,7 +404,7 @@ export class ProjectActions {
|
||||
|
||||
const [newSpec] = codeGenResults.files
|
||||
|
||||
const cfg = this.ctx.project.getConfig()
|
||||
const cfg = await this.ctx.project.getConfig()
|
||||
|
||||
if (cfg && this.ctx.currentProject) {
|
||||
const testingType = (codeGenType === 'component' || codeGenType === 'story') ? 'component' : 'e2e'
|
||||
|
||||
@@ -51,8 +51,8 @@ export interface InjectedConfigApi {
|
||||
allowedConfig(config: Cypress.ConfigOptions): Cypress.ConfigOptions
|
||||
updateWithPluginValues(config: FullConfig, modifiedConfig: Partial<Cypress.ConfigOptions>): FullConfig
|
||||
setupFullConfigWithDefaults(config: SetupFullConfigOptions): Promise<FullConfig>
|
||||
validateRootConfigBreakingChanges<T extends Cypress.ConfigOptions>(config: Partial<T>, onWarning: BreakingValidationFn<CypressError>, onErr: BreakingValidationFn<never>): T
|
||||
validateTestingTypeConfigBreakingChanges<T extends Cypress.ConfigOptions>(config: Partial<T>, testingType: Cypress.TestingType, onWarning: BreakingValidationFn<CypressError>, onErr: BreakingValidationFn<never>): T
|
||||
validateRootConfigBreakingChanges<T extends Cypress.ConfigOptions>(config: Partial<T>, onWarning: BreakingValidationFn<CypressError>, onErr: BreakingValidationFn<never>): void
|
||||
validateTestingTypeConfigBreakingChanges<T extends Cypress.ConfigOptions>(config: Partial<T>, testingType: Cypress.TestingType, onWarning: BreakingValidationFn<CypressError>, onErr: BreakingValidationFn<never>): void
|
||||
}
|
||||
|
||||
type State<S, V = undefined> = V extends undefined ? {state: S, value?: V } : {state: S, value: V}
|
||||
|
||||
@@ -119,8 +119,8 @@ export class ProjectDataSource {
|
||||
return path.basename(projectRoot)
|
||||
}
|
||||
|
||||
getConfig () {
|
||||
return this.ctx.lifecycleManager.loadedFullConfig
|
||||
async getConfig () {
|
||||
return await this.ctx.lifecycleManager.getFullInitialConfig()
|
||||
}
|
||||
|
||||
getCurrentProjectSavedState () {
|
||||
@@ -145,11 +145,7 @@ export class ProjectDataSource {
|
||||
}> {
|
||||
const toArray = (val?: string | string[]) => val ? typeof val === 'string' ? [val] : val : undefined
|
||||
|
||||
const config = this.getConfig()
|
||||
|
||||
if (!config) {
|
||||
throw Error(`Config for ${projectRoot} was not loaded`)
|
||||
}
|
||||
const config = await this.getConfig()
|
||||
|
||||
return {
|
||||
specPattern: toArray(config[testingType]?.specPattern),
|
||||
|
||||
@@ -33,7 +33,9 @@ export function createTestDataContext (mode: DataContextConfig['mode'] = 'run')
|
||||
getServerPluginHandlers: () => [],
|
||||
} as InjectedConfigApi,
|
||||
projectApi: {} as ProjectApiShape,
|
||||
electronApi: {} as ElectronApiShape,
|
||||
electronApi: {
|
||||
copyTextToClipboard: (text) => {},
|
||||
} as ElectronApiShape,
|
||||
browserApi: {} as BrowserApiShape,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CopyButton from './CopyButton.vue'
|
||||
import CopyButton from '../gql-components/CopyButton.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
|
||||
@@ -52,7 +52,7 @@ shikiWrapperClasses computed property.
|
||||
|
||||
props.class,
|
||||
]"
|
||||
@click="copyOnClick && isSupported ? () => copyCode() : () => {}"
|
||||
@click="copyOnClick ? () => copyCode() : () => {}"
|
||||
v-html="highlightedCode"
|
||||
/>
|
||||
<pre
|
||||
@@ -61,7 +61,7 @@ shikiWrapperClasses computed property.
|
||||
:class="[props.class, lineNumbers ? 'pl-56px' : 'pl-8px' ]"
|
||||
>{{ trimmedCode }}</pre>
|
||||
<CopyButton
|
||||
v-if="copyButton && isSupported"
|
||||
v-if="copyButton"
|
||||
variant="outline"
|
||||
tabindex="-1"
|
||||
class="bg-white absolute"
|
||||
@@ -107,8 +107,8 @@ export { highlighter, inheritAttrs }
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onBeforeMount, ref } from 'vue'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import CopyButton from './CopyButton.vue'
|
||||
import CopyButton from '../gql-components/CopyButton.vue'
|
||||
import { useClipboard } from '../gql-components/useClipboard'
|
||||
|
||||
const highlighterInitialized = ref(false)
|
||||
|
||||
@@ -158,7 +158,7 @@ const highlightedCode = computed(() => {
|
||||
|
||||
const codeEl: Ref<HTMLElement | null> = ref(null)
|
||||
|
||||
const { copy, isSupported } = useClipboard()
|
||||
const { copy } = useClipboard()
|
||||
|
||||
const copyCode = () => {
|
||||
if (codeEl.value) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CopyButton from './CopyButton.vue'
|
||||
import CopyButton from '../gql-components/CopyButton.vue'
|
||||
|
||||
defineProps<{
|
||||
projectFolderName?: string
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<Button
|
||||
v-if="isSupported"
|
||||
:size="size"
|
||||
:variant="variant"
|
||||
data-cy="copy-button"
|
||||
@@ -26,11 +25,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useClipboard } from './useClipboard'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import type { ButtonSizes, ButtonVariants } from '../components/Button.vue'
|
||||
import Button from '../components/Button.vue'
|
||||
import TransitionQuickFade from '../components/transitions/TransitionQuickFade.vue'
|
||||
import type { ButtonSizes, ButtonVariants } from '@cy/components/Button.vue'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import TransitionQuickFade from '@cy/components/transitions/TransitionQuickFade.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
text: string
|
||||
@@ -43,7 +42,7 @@ const props = withDefaults(defineProps<{
|
||||
size: 'md',
|
||||
})
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard({ copiedDuring: 2000 })
|
||||
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
|
||||
const copyToClipboard = () => {
|
||||
if (props.text) {
|
||||
copy(props.text)
|
||||
42
packages/frontend-shared/src/gql-components/useClipboard.ts
Normal file
42
packages/frontend-shared/src/gql-components/useClipboard.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Using the clipboard API from electron instead of the browser's navigator API,
|
||||
* we avoid the safety measures from the browser.
|
||||
* This way, regardless of the browser, we can use and test the clipboard.
|
||||
*/
|
||||
import { Clipboard_CopyToClipboardDocument } from '../generated/graphql'
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
gql`
|
||||
mutation Clipboard_CopyToClipboard($text: String!) {
|
||||
copyTextToClipboard(text: $text)
|
||||
}
|
||||
`
|
||||
|
||||
interface ClipboardOptions {
|
||||
// time it takes in MS for the copied message to disappear
|
||||
copiedDuring?: number
|
||||
}
|
||||
|
||||
export function useClipboard (options: ClipboardOptions = {}) {
|
||||
const copyMutation = useMutation(Clipboard_CopyToClipboardDocument)
|
||||
const copied = ref(false)
|
||||
|
||||
let timer: NodeJS.Timeout | undefined
|
||||
|
||||
const copy = async (text: string) => {
|
||||
const { data } = await copyMutation.executeMutation({ text })
|
||||
|
||||
copied.value = data?.copyTextToClipboard ?? false
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
copied.value = false
|
||||
timer = undefined
|
||||
}, options.copiedDuring || 2000)
|
||||
}
|
||||
|
||||
return { copy, copied }
|
||||
}
|
||||
@@ -904,6 +904,9 @@ type Mutation {
|
||||
cloudProjectRequestAccess(projectSlug: String!): CloudProjectResult
|
||||
completeSetup: Query
|
||||
|
||||
"""add the passed text to the local clipboard"""
|
||||
copyTextToClipboard(text: String!): Boolean
|
||||
|
||||
"""
|
||||
Development only: Triggers or dismisses a prompted refresh by touching the file watched by our development scripts
|
||||
"""
|
||||
|
||||
@@ -11,8 +11,21 @@ import { ScaffoldedFile } from './gql-ScaffoldedFile'
|
||||
|
||||
export const mutation = mutationType({
|
||||
definition (t) {
|
||||
t.field('copyTextToClipboard', {
|
||||
type: 'Boolean',
|
||||
description: 'add the passed text to the local clipboard',
|
||||
args: {
|
||||
text: nonNull(stringArg()),
|
||||
},
|
||||
resolve: (_, { text }, ctx) => {
|
||||
ctx.electronApi.copyTextToClipboard(text)
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
t.field('reinitializeCypress', {
|
||||
type: 'Query',
|
||||
type: Query,
|
||||
description: 'Re-initializes Cypress from the initial CLI options',
|
||||
resolve: async (_, args, ctx) => {
|
||||
await ctx.reinitializeCypress(ctx.modeOptions)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import sinon from 'sinon'
|
||||
import { ManualInstallFragmentDoc } from '../generated/graphql-test'
|
||||
import ManualInstall from './ManualInstall.vue'
|
||||
import { CYPRESS_REACT_LATEST, CYPRESS_WEBPACK } from '@packages/scaffold-config'
|
||||
import { Clipboard_CopyToClipboardDocument } from '../generated/graphql'
|
||||
|
||||
describe('<ManualInstall />', () => {
|
||||
it('playground', () => {
|
||||
@@ -17,6 +19,8 @@ describe('<ManualInstall />', () => {
|
||||
const framework = CYPRESS_REACT_LATEST
|
||||
const bundler = CYPRESS_WEBPACK
|
||||
|
||||
const stubCopy = sinon.stub()
|
||||
|
||||
cy.mountFragment(ManualInstallFragmentDoc, {
|
||||
render: (gqlVal) => (
|
||||
<div class="rounded border-1 border-gray-400 m-10">
|
||||
@@ -25,30 +29,21 @@ describe('<ManualInstall />', () => {
|
||||
),
|
||||
})
|
||||
|
||||
cy.stubMutationResolver(Clipboard_CopyToClipboardDocument, (defineResult, { text }) => {
|
||||
stubCopy(text)
|
||||
|
||||
return defineResult({
|
||||
copyTextToClipboard: true,
|
||||
})
|
||||
})
|
||||
|
||||
const installCommand = `npm install -D @cypress/react @cypress/webpack-dev-server`
|
||||
|
||||
// @ts-ignore
|
||||
cy.findByRole('button', { name: 'Copy' }).realClick()
|
||||
cy.findByText(installCommand).should('be.visible')
|
||||
cy.findByRole('button', { name: 'Copy' }).click()
|
||||
cy.findByRole('button', { name: 'Copied!' }).should('be.visible')
|
||||
|
||||
cy.findByRole('button', { name: 'Copied!' })
|
||||
|
||||
if (Cypress.config('browser').name === 'chrome') {
|
||||
cy.wrap(Cypress.automation('remote:debugger:protocol', {
|
||||
command: 'Browser.grantPermissions',
|
||||
params: {
|
||||
permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'],
|
||||
origin: window.location.origin,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
cy.window().its('navigator.permissions')
|
||||
.invoke('query', { name: 'clipboard-read' })
|
||||
.its('state').then(cy.log)
|
||||
|
||||
cy.window().its('navigator.clipboard')
|
||||
.invoke('readText')
|
||||
.should('equal', installCommand)
|
||||
cy.wrap(stubCopy).should('have.been.calledWith', installCommand)
|
||||
|
||||
const validatePackage = (packageName: string) => {
|
||||
cy.findByRole('link', { name: packageName })
|
||||
|
||||
@@ -168,6 +168,9 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
|
||||
showSaveDialog (window: BrowserWindow, props: SaveDialogOptions) {
|
||||
return electron.dialog.showSaveDialog(window, props)
|
||||
},
|
||||
copyTextToClipboard (text: string) {
|
||||
electron.clipboard.writeText(text)
|
||||
},
|
||||
},
|
||||
localSettingsApi: {
|
||||
async setPreferences (object: AllowedState) {
|
||||
|
||||
Reference in New Issue
Block a user