refactor: monorepo+pnpm (removed husky)

This commit is contained in:
Andrew Bastin
2021-09-10 00:28:28 +05:30
parent 917550ff4d
commit b28f82a881
445 changed files with 81301 additions and 63752 deletions

View File

@@ -0,0 +1,95 @@
<template>
<div>
<div class="field-title" :class="{ 'field-highlighted': isHighlighted }">
{{ fieldName }}
<span v-if="fieldArgs.length > 0">
(
<span v-for="(field, index) in fieldArgs" :key="`field-${index}`">
{{ field.name }}:
<GraphqlTypeLink
:gql-type="field.type"
:jump-type-callback="jumpTypeCallback"
/>
<span v-if="index !== fieldArgs.length - 1">, </span>
</span>
) </span
>:
<GraphqlTypeLink
:gql-type="gqlField.type"
:jump-type-callback="jumpTypeCallback"
/>
</div>
<div
v-if="gqlField.description"
class="text-secondaryLight py-2 field-desc"
>
{{ gqlField.description }}
</div>
<div
v-if="gqlField.isDeprecated"
class="
rounded
bg-yellow-200
my-1
text-black
py-1
px-2
inline-block
field-deprecated
"
>
{{ $t("state.deprecated") }}
</div>
<div v-if="fieldArgs.length > 0">
<h5 class="my-2">Arguments:</h5>
<div class="border-divider border-l-2 pl-4">
<div v-for="(field, index) in fieldArgs" :key="`field-${index}`">
<span>
{{ field.name }}:
<GraphqlTypeLink
:gql-type="field.type"
:jump-type-callback="jumpTypeCallback"
/>
</span>
<div
v-if="field.description"
class="text-secondaryLight py-2 field-desc"
>
{{ field.description }}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
export default defineComponent({
props: {
gqlField: { type: Object, default: () => {} },
jumpTypeCallback: { type: Function, default: () => {} },
isHighlighted: { type: Boolean, default: false },
},
computed: {
fieldName() {
return this.gqlField.name
},
fieldArgs() {
return this.gqlField.args || []
},
},
})
</script>
<style scoped lang="scss">
.field-highlighted {
@apply border-b-2 border-accent;
}
.field-title {
@apply select-text;
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<div class="opacity-0 show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import "ace-builds/src-noconflict/ext-language_tools"
import "ace-builds/src-noconflict/mode-graphqlschema"
import * as gql from "graphql"
import { getAutocompleteSuggestions } from "graphql-language-service-interface"
import { defineComponent } from "@nuxtjs/composition-api"
import { defineGQLLanguageMode } from "~/helpers/syntax/gqlQueryLangMode"
import debounce from "~/helpers/utils/debounce"
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
onRunGQLQuery: {
type: Function,
default: () => {},
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
validationSchema: null,
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
defineGQLLanguageMode(ace)
const langTools = ace.require("ace/ext/language_tools")
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/gql-query`,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
const completer = {
getCompletions: (
editor,
_session,
{ row, column },
_prefix,
callback
) => {
if (this.validationSchema) {
const completions = getAutocompleteSuggestions(
this.validationSchema,
editor.getValue(),
{
line: row,
character: column,
}
)
callback(
null,
completions.map(({ label, detail }) => ({
name: label,
value: label,
score: 1.0,
meta: detail,
}))
)
} else {
callback(null, [])
}
},
}
langTools.setCompleters([completer])
if (this.value) editor.setValue(this.value, 1)
this.editor = editor
this.cacheValue = this.value
editor.commands.addCommand({
name: "runGQLQuery",
exec: () => this.onRunGQLQuery(this.editor.getValue()),
bindKey: {
mac: "cmd-enter",
win: "ctrl-enter",
},
})
editor.commands.addCommand({
name: "prettifyGQLQuery",
exec: () => this.prettifyQuery(),
bindKey: {
mac: "cmd-p",
win: "ctrl-p",
},
})
editor.on("change", () => {
const content = editor.getValue()
this.$emit("input", content)
this.parseContents(content)
this.cacheValue = content
})
this.parseContents(this.value)
},
beforeDestroy() {
this.editor.destroy()
},
methods: {
prettifyQuery() {
try {
this.$emit("update-query", gql.print(gql.parse(this.editor.getValue())))
} catch (e) {
this.$toast.error(this.$t("error.gql_prettify_invalid_query"), {
icon: "error_outline",
})
}
},
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
setValidationSchema(schema) {
this.validationSchema = schema
this.parseContents(this.cacheValue)
},
parseContents: debounce(function (content) {
if (content !== "") {
try {
const doc = gql.parse(content)
if (this.validationSchema) {
this.editor.session.setAnnotations(
gql
.validate(this.validationSchema, doc)
.map(({ locations, message }) => ({
row: locations[0].line - 1,
column: locations[0].column - 1,
text: message,
type: "error",
}))
)
}
} catch (e) {
this.editor.session.setAnnotations([
{
row: e.locations[0].line - 1,
column: e.locations[0].column - 1,
text: e.message,
type: "error",
},
])
}
} else {
this.editor.session.setAnnotations([])
}
}, 2000),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="bg-primary flex p-4 top-0 z-10 sticky">
<div class="space-x-2 flex-1 inline-flex">
<input
id="url"
v-model="url"
v-focus
type="url"
autocomplete="off"
spellcheck="false"
class="
bg-primaryLight
border border-divider
rounded
text-secondaryDark
w-full
py-2
px-4
hover:border-dividerDark
focus-visible:bg-transparent focus-visible:border-dividerDark
"
:placeholder="$t('request.url')"
@keyup.enter="onConnectClick"
/>
<ButtonPrimary
id="get"
name="get"
:label="!connected ? $t('action.connect') : $t('action.disconnect')"
class="w-32"
@click.native="onConnectClick"
/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "@nuxtjs/composition-api"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { GQLConnection } from "~/helpers/GQLConnection"
import { getCurrentStrategyID } from "~/helpers/network"
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup(props) {
const connected = useReadonlyStream(props.conn.connected$, false)
const headers = useReadonlyStream(gqlHeaders$, [])
const url = useStream(gqlURL$, "", setGQLURL)
const onConnectClick = () => {
if (!connected.value) {
props.conn.connect(url.value, headers.value as any)
logHoppRequestRunToAnalytics({
platform: "graphql-schema",
strategy: getCurrentStrategyID(),
})
} else {
props.conn.disconnect()
}
}
return {
url,
connected,
onConnectClick,
}
},
})
</script>

View File

@@ -0,0 +1,555 @@
<template>
<div>
<SmartTabs styles="sticky bg-primary top-upperPrimaryStickyFold z-10">
<SmartTab :id="'query'" :label="$t('tab.query')" :selected="true">
<AppSection label="query">
<div
class="
bg-primary
border-b border-dividerLight
flex flex-1
top-upperSecondaryStickyFold
pl-4
z-10
sticky
items-center
justify-between
gqlRunQuery
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("request.query") }}
</label>
<div class="flex">
<ButtonSecondary
:label="$t('request.run')"
svg="play"
class="rounded-none !text-accent"
@click.native="runQuery()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io"
blank
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.copy')"
:svg="copyQueryIcon"
@click.native="copyQuery"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="`${$t(
'action.prettify'
)} <kbd>${getSpecialKey()}</kbd><kbd>P</kbd>`"
:svg="prettifyQueryIcon"
@click.native="prettifyQuery"
/>
<ButtonSecondary
ref="saveRequest"
v-tippy="{ theme: 'tooltip' }"
:title="$t('request.save')"
svg="folder-plus"
@click.native="saveRequest"
/>
</div>
</div>
<GraphqlQueryEditor
ref="queryEditor"
v-model="gqlQueryString"
:on-run-g-q-l-query="runQuery"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
@update-query="updateQuery"
/>
</AppSection>
</SmartTab>
<SmartTab :id="'variables'" :label="$t('tab.variables')">
<AppSection label="variables">
<div
class="
bg-primary
border-b border-dividerLight
flex flex-1
top-upperSecondaryStickyFold
pl-4
z-10
sticky
items-center
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("request.variables") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io"
blank
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.copy')"
:svg="copyVariablesIcon"
@click.native="copyVariables"
/>
</div>
</div>
<SmartAceEditor
ref="variableEditor"
v-model="variableString"
:lang="'json'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</AppSection>
</SmartTab>
<SmartTab :id="'headers'" :label="$t('tab.headers')">
<AppSection label="headers">
<div
class="
bg-primary
border-b border-dividerLight
flex flex-1
top-upperSecondaryStickyFold
pl-4
z-10
sticky
items-center
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("tab.headers") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io"
blank
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear_all')"
svg="trash-2"
:disabled="bulkMode"
@click.native="headers = []"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.bulk_mode')"
svg="edit"
:class="{ '!text-accent': bulkMode }"
@click.native="bulkMode = !bulkMode"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('add.new')"
svg="plus"
:disabled="bulkMode"
@click.native="addRequestHeader"
/>
</div>
</div>
<div v-if="bulkMode" class="flex">
<textarea-autosize
v-model="bulkHeaders"
v-focus
name="bulk-parameters"
class="
bg-transparent
border-b border-dividerLight
flex
font-mono
flex-1
py-2
px-4
whitespace-pre
resize-y
overflow-auto
"
rows="10"
:placeholder="$t('state.bulk_mode_placeholder')"
/>
</div>
<div v-else>
<div
v-for="(header, index) in headers"
:key="`header-${index}`"
class="
divide-x divide-dividerLight
border-b border-dividerLight
flex
"
>
<SmartAutoComplete
:placeholder="$t('count.header', { count: index + 1 })"
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles="
bg-transparent
flex
flex-1
py-1
px-4
truncate
focus:outline-none
"
@input="
updateGQLHeader(index, {
key: $event,
value: header.value,
active: header.active,
})
"
/>
<input
class="bg-transparent flex flex-1 py-2 px-4"
:placeholder="$t('count.value', { count: index + 1 })"
:name="`value ${index}`"
:value="header.value"
autofocus
@change="
updateGQLHeader(index, {
key: header.key,
value: $event.target.value,
active: header.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
header.hasOwnProperty('active')
? header.active
? $t('action.turn_off')
: $t('action.turn_on')
: $t('action.turn_off')
"
:svg="
header.hasOwnProperty('active')
? header.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateGQLHeader(index, {
key: header.key,
value: header.value,
active: !header.active,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')"
svg="trash"
color="red"
@click.native="removeRequestHeader(index)"
/>
</span>
</div>
<div
v-if="headers.length === 0"
class="
flex flex-col
text-secondaryLight
p-4
items-center
justify-center
"
>
<span class="text-center pb-4">
{{ $t("empty.headers") }}
</span>
<ButtonSecondary
:label="$t('add.new')"
filled
svg="plus"
@click.native="addRequestHeader"
/>
</div>
</div>
</AppSection>
</SmartTab>
</SmartTabs>
<CollectionsSaveRequest
mode="graphql"
:show="showSaveRequestModal"
@hide-modal="hideRequestModal"
/>
</div>
</template>
<script lang="ts">
import {
defineComponent,
onMounted,
PropType,
ref,
useContext,
watch,
} from "@nuxtjs/composition-api"
import clone from "lodash/clone"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import {
useNuxt,
useReadonlyStream,
useStream,
} from "~/helpers/utils/composables"
import {
addGQLHeader,
gqlHeaders$,
gqlQuery$,
gqlResponse$,
gqlURL$,
gqlVariables$,
removeGQLHeader,
setGQLHeaders,
setGQLQuery,
setGQLResponse,
setGQLVariables,
updateGQLHeader,
} from "~/newstore/GQLSession"
import { commonHeaders } from "~/helpers/headers"
import { GQLConnection } from "~/helpers/GQLConnection"
import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { getCurrentStrategyID } from "~/helpers/network"
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup(props) {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const nuxt = useNuxt()
const bulkMode = ref(false)
const bulkHeaders = ref("")
watch(bulkHeaders, () => {
try {
const transformation = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setGQLHeaders(transformation)
} catch (e) {
$toast.error(t("error.something_went_wrong").toString(), {
icon: "error_outline",
})
console.error(e)
}
})
const url = useReadonlyStream(gqlURL$, "")
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
const variableString = useStream(gqlVariables$, "", setGQLVariables)
const headers = useStream(gqlHeaders$, [], setGQLHeaders)
const queryEditor = ref<any | null>(null)
const copyQueryIcon = ref("copy")
const prettifyQueryIcon = ref("align-left")
const copyVariablesIcon = ref("copy")
const showSaveRequestModal = ref(false)
const schema = useReadonlyStream(props.conn.schemaString$, "")
watch(
headers,
() => {
if (
(headers.value[headers.value.length - 1]?.key !== "" ||
headers.value[headers.value.length - 1]?.value !== "") &&
headers.value.length
)
addRequestHeader()
},
{ deep: true }
)
onMounted(() => {
if (!headers.value?.length) {
addRequestHeader()
}
})
const copyQuery = () => {
copyToClipboard(gqlQueryString.value)
copyQueryIcon.value = "check"
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
}
const response = useStream(gqlResponse$, "", setGQLResponse)
const runQuery = async () => {
const startTime = Date.now()
nuxt.value.$loading.start()
response.value = t("state.loading").toString()
try {
const runURL = clone(url.value)
const runHeaders = clone(headers.value)
const runQuery = clone(gqlQueryString.value)
const runVariables = clone(variableString.value)
const responseText = await props.conn.runQuery(
runURL,
runHeaders,
runQuery,
runVariables
)
const duration = Date.now() - startTime
nuxt.value.$loading.finish()
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
addGraphqlHistoryEntry(
makeGQLHistoryEntry({
request: makeGQLRequest({
name: "",
url: runURL,
query: runQuery,
headers: runHeaders,
variables: runVariables,
}),
response: response.value,
star: false,
})
)
$toast.success(t("state.finished_in", { duration }).toString(), {
icon: "done",
})
} catch (e: any) {
response.value = `${e}. ${t("error.check_console_details")}`
nuxt.value.$loading.finish()
$toast.error(`${e} ${t("error.f12_details").toString()}`, {
icon: "error_outline",
})
console.error(e)
}
logHoppRequestRunToAnalytics({
platform: "graphql-query",
strategy: getCurrentStrategyID(),
})
}
const hideRequestModal = () => {
showSaveRequestModal.value = false
}
const prettifyQuery = () => {
queryEditor.value.prettifyQuery()
prettifyQueryIcon.value = "check"
setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000)
}
const saveRequest = () => {
showSaveRequestModal.value = true
}
// Why ?
const updateQuery = (updatedQuery: string) => {
gqlQueryString.value = updatedQuery
}
const copyVariables = () => {
copyToClipboard(variableString.value)
copyVariablesIcon.value = "check"
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
}
const addRequestHeader = () => {
addGQLHeader({
key: "",
value: "",
active: true,
})
}
const removeRequestHeader = (index: number) => {
removeGQLHeader(index)
}
return {
gqlQueryString,
variableString,
headers,
copyQueryIcon,
prettifyQueryIcon,
copyVariablesIcon,
queryEditor,
showSaveRequestModal,
hideRequestModal,
schema,
copyQuery,
runQuery,
prettifyQuery,
saveRequest,
updateQuery,
copyVariables,
addRequestHeader,
removeRequestHeader,
getSpecialKey: getPlatformSpecialKey,
commonHeaders,
updateGQLHeader,
bulkMode,
bulkHeaders,
}
},
})
</script>

View File

@@ -0,0 +1,188 @@
<template>
<AppSection ref="response" label="response">
<div
v-if="responseString"
class="
bg-primary
border-b border-dividerLight
flex flex-1
pl-4
top-0
z-10
sticky
items-center
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("response.title") }}
</label>
<div class="flex">
<ButtonSecondary
ref="downloadResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
:svg="downloadResponseIcon"
@click.native="downloadResponse"
/>
<ButtonSecondary
ref="copyResponseButton"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.copy')"
:svg="copyResponseIcon"
@click.native="copyResponse"
/>
</div>
</div>
<SmartAceEditor
v-if="responseString"
:value="responseString"
:lang="'json'"
:lint="false"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div
v-else
class="
flex flex-col flex-1
text-secondaryLight
p-4
items-center
justify-center
"
>
<div class="flex space-x-2 pb-4">
<div class="flex flex-col space-y-4 items-end">
<span class="flex flex-1 items-center">
{{ $t("shortcut.request.send_request") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.show_all") }}
</span>
<!-- <span class="flex flex-1 items-center">
{{ $t("shortcut.general.command_menu") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.help_menu") }}
</span> -->
</div>
<div class="flex flex-col space-y-4">
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">G</span>
</div>
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">K</span>
</div>
<!-- <div class="flex">
<span class="shortcut-key">/</span>
</div>
<div class="flex">
<span class="shortcut-key">?</span>
</div> -->
</div>
</div>
<ButtonSecondary
:label="$t('app.documentation')"
to="https://docs.hoppscotch.io"
svg="external-link"
blank
outline
reverse
/>
</div>
</AppSection>
</template>
<script lang="ts">
import {
defineComponent,
PropType,
ref,
useContext,
} from "@nuxtjs/composition-api"
import { GQLConnection } from "~/helpers/GQLConnection"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { gqlResponse$ } from "~/newstore/GQLSession"
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup() {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const responseString = useReadonlyStream(gqlResponse$, "")
const downloadResponseIcon = ref("download")
const copyResponseIcon = ref("copy")
const copyResponse = () => {
copyToClipboard(responseString.value!)
copyResponseIcon.value = "check"
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
}
const downloadResponse = () => {
const dataToWrite = responseString.value
const file = new Blob([dataToWrite!], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadResponseIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadResponseIcon.value = "download"
}, 1000)
}
return {
responseString,
downloadResponseIcon,
copyResponseIcon,
downloadResponse,
copyResponse,
getSpecialKey: getPlatformSpecialKey,
}
},
})
</script>
<style lang="scss" scoped>
.shortcut-key {
@apply bg-dividerLight;
@apply rounded;
@apply ml-2;
@apply py-1;
@apply px-2;
@apply inline-flex;
}
</style>

View File

@@ -0,0 +1,470 @@
<template>
<aside>
<SmartTabs styles="sticky bg-primary z-10 top-0">
<SmartTab :id="'docs'" :label="`Docs`" :selected="true">
<AppSection label="docs">
<div class="bg-primary flex top-sidebarPrimaryStickyFold z-10 sticky">
<input
v-model="graphqlFieldsFilterText"
type="search"
autocomplete="off"
:placeholder="$t('action.search')"
class="bg-transparent flex w-full p-4 py-2"
/>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/quickstart/graphql"
blank
:title="$t('app.wiki')"
svg="help-circle"
/>
</div>
</div>
<SmartTabs
ref="gqlTabs"
styles="border-t border-dividerLight bg-primary sticky z-10 top-sidebarSecondaryStickyFold"
>
<div class="gqlTabs">
<SmartTab
v-if="queryFields.length > 0"
:id="'queries'"
:label="$t('tab.queries')"
:selected="true"
class="divide-y divide-dividerLight"
>
<GraphqlField
v-for="(field, index) in filteredQueryFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
/>
</SmartTab>
<SmartTab
v-if="mutationFields.length > 0"
:id="'mutations'"
:label="$t('graphql.mutations')"
class="divide-y divide-dividerLight"
>
<GraphqlField
v-for="(field, index) in filteredMutationFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
/>
</SmartTab>
<SmartTab
v-if="subscriptionFields.length > 0"
:id="'subscriptions'"
:label="$t('graphql.subscriptions')"
class="divide-y divide-dividerLight"
>
<GraphqlField
v-for="(field, index) in filteredSubscriptionFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
/>
</SmartTab>
<SmartTab
v-if="graphqlTypes.length > 0"
:id="'types'"
ref="typesTab"
:label="$t('tab.types')"
class="divide-y divide-dividerLight"
>
<GraphqlType
v-for="(type, index) in filteredGraphqlTypes"
:key="`type-${index}`"
:gql-type="type"
:gql-types="graphqlTypes"
:is-highlighted="isGqlTypeHighlighted(type)"
:highlighted-fields="getGqlTypeHighlightedFields(type)"
:jump-type-callback="handleJumpToType"
/>
</SmartTab>
</div>
</SmartTabs>
<div
v-if="
queryFields.length === 0 &&
mutationFields.length === 0 &&
subscriptionFields.length === 0 &&
graphqlTypes.length === 0
"
class="
flex flex-col
text-secondaryLight
p-4
items-center
justify-center
"
>
<i class="opacity-75 pb-2 material-icons">link</i>
<span class="text-center">
{{ $t("empty.schema") }}
</span>
</div>
</AppSection>
</SmartTab>
<SmartTab :id="'history'" :label="$t('tab.history')">
<History
ref="graphqlHistoryComponent"
:page="'graphql'"
@useHistory="handleUseHistory"
/>
</SmartTab>
<SmartTab :id="'collections'" :label="$t('tab.collections')">
<CollectionsGraphql />
</SmartTab>
<SmartTab :id="'schema'" :label="`Schema`">
<AppSection ref="schema" label="schema">
<div
v-if="schemaString"
class="
bg-primary
flex flex-1
top-sidebarPrimaryStickyFold
pl-4
z-10
sticky
items-center
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("graphql.schema") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/quickstart/graphql"
blank
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
ref="downloadSchema"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
:svg="downloadSchemaIcon"
@click.native="downloadSchema"
/>
<ButtonSecondary
ref="copySchemaCode"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.copy')"
:svg="copySchemaIcon"
@click.native="copySchema"
/>
</div>
</div>
<SmartAceEditor
v-if="schemaString"
v-model="schemaString"
:lang="'graphqlschema'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div
v-else
class="
flex flex-col
text-secondaryLight
p-4
items-center
justify-center
"
>
<i class="opacity-75 pb-2 material-icons">link</i>
<span class="text-center">
{{ $t("empty.schema") }}
</span>
</div>
</AppSection>
</SmartTab>
</SmartTabs>
</aside>
</template>
<script lang="ts">
import {
computed,
defineComponent,
nextTick,
PropType,
ref,
useContext,
} from "@nuxtjs/composition-api"
import { GraphQLField, GraphQLType } from "graphql"
import { map } from "rxjs/operators"
import { GQLConnection } from "~/helpers/GQLConnection"
import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {
setGQLHeaders,
setGQLQuery,
setGQLResponse,
setGQLURL,
setGQLVariables,
} from "~/newstore/GQLSession"
function isTextFoundInGraphqlFieldObject(
text: string,
field: GraphQLField<any, any>
) {
const normalizedText = text.toLowerCase()
const isFilterTextFoundInDescription = field.description
? field.description.toLowerCase().includes(normalizedText)
: false
const isFilterTextFoundInName = field.name
.toLowerCase()
.includes(normalizedText)
return isFilterTextFoundInDescription || isFilterTextFoundInName
}
function getFilteredGraphqlFields(
filterText: string,
fields: GraphQLField<any, any>[]
) {
if (!filterText) return fields
return fields.filter((field) =>
isTextFoundInGraphqlFieldObject(filterText, field)
)
}
function getFilteredGraphqlTypes(filterText: string, types: GraphQLType[]) {
if (!filterText) return types
return types.filter((type) => {
const isFilterTextMatching = isTextFoundInGraphqlFieldObject(
filterText,
type as any
)
if (isFilterTextMatching) {
return true
}
const isFilterTextMatchingAtLeastOneField = Object.values(
(type as any)._fields || {}
).some((field) => isTextFoundInGraphqlFieldObject(filterText, field as any))
return isFilterTextMatchingAtLeastOneField
})
}
function resolveRootType(type: GraphQLType) {
let t: any = type
while (t.ofType) t = t.ofType
return t
}
type GQLHistoryEntry = {
url: string
headers: GQLHeader[]
query: string
response: string
variables: string
}
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup(props) {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const queryFields = useReadonlyStream(
props.conn.queryFields$.pipe(map((x) => x ?? [])),
[]
)
const mutationFields = useReadonlyStream(
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
[]
)
const subscriptionFields = useReadonlyStream(
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
[]
)
const graphqlTypes = useReadonlyStream(
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
[]
)
const downloadSchemaIcon = ref("download")
const copySchemaIcon = ref("copy")
const graphqlFieldsFilterText = ref("")
const gqlTabs = ref<any | null>(null)
const typesTab = ref<any | null>(null)
const filteredQueryFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
queryFields.value as any
)
})
const filteredMutationFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
mutationFields.value as any
)
})
const filteredSubscriptionFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
subscriptionFields.value as any
)
})
const filteredGraphqlTypes = computed(() => {
return getFilteredGraphqlTypes(
graphqlFieldsFilterText.value,
graphqlTypes.value as any
)
})
const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
if (!graphqlFieldsFilterText.value) return false
return isTextFoundInGraphqlFieldObject(
graphqlFieldsFilterText.value,
gqlType as any
)
}
const getGqlTypeHighlightedFields = (gqlType: GraphQLType) => {
if (!graphqlFieldsFilterText.value) return []
const fields = Object.values((gqlType as any)._fields || {})
if (!fields || fields.length === 0) return []
return fields.filter((field) =>
isTextFoundInGraphqlFieldObject(
graphqlFieldsFilterText.value,
field as any
)
)
}
const handleJumpToType = async (type: GraphQLType) => {
gqlTabs.value.selectTab(typesTab.value)
await nextTick()
const rootTypeName = resolveRootType(type).name
const target = document.getElementById(`type_${rootTypeName}`)
if (target) {
gqlTabs.value.$el
.querySelector(".gqlTabs")
.scrollTo({ top: target.offsetTop, behavior: "smooth" })
}
}
const schemaString = useReadonlyStream(
props.conn.schemaString$.pipe(map((x) => x ?? "")),
""
)
const downloadSchema = () => {
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
const file = new Blob([dataToWrite], { type: "application/graphql" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${
url.split("/").pop()!.split("#")[0].split("?")[0]
}.graphql`
document.body.appendChild(a)
a.click()
downloadSchemaIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadSchemaIcon.value = "download"
}, 1000)
}
const copySchema = () => {
if (!schemaString.value) return
copyToClipboard(schemaString.value)
copySchemaIcon.value = "check"
setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
}
const handleUseHistory = (entry: GQLHistoryEntry) => {
const url = entry.url
const headers = entry.headers
const gqlQueryString = entry.query
const variableString = entry.variables
const responseText = entry.response
setGQLURL(url)
setGQLHeaders(headers)
setGQLQuery(gqlQueryString)
setGQLVariables(variableString)
setGQLResponse(responseText)
props.conn.reset()
}
return {
queryFields,
mutationFields,
subscriptionFields,
graphqlTypes,
schemaString,
graphqlFieldsFilterText,
filteredQueryFields,
filteredMutationFields,
filteredSubscriptionFields,
filteredGraphqlTypes,
isGqlTypeHighlighted,
getGqlTypeHighlightedFields,
gqlTabs,
typesTab,
handleJumpToType,
downloadSchema,
downloadSchemaIcon,
copySchemaIcon,
copySchema,
handleUseHistory,
}
},
})
</script>

View File

@@ -0,0 +1,107 @@
<template>
<div :id="`type_${gqlType.name}`" class="p-4">
<div class="type-title" :class="{ 'text-accent': isHighlighted }">
<span v-if="isInput" class="text-accent">input </span>
<span v-else-if="isInterface" class="text-accent">interface </span>
<span v-else-if="isEnum" class="text-accent">enum </span>
{{ gqlType.name }}
</div>
<div v-if="gqlType.description" class="text-secondaryLight py-2 type-desc">
{{ gqlType.description }}
</div>
<div v-if="interfaces.length > 0">
<h5 class="my-2">Interfaces:</h5>
<div
v-for="(gqlInterface, index) in interfaces"
:key="`gqlInterface-${index}`"
>
<GraphqlTypeLink
:gql-type="gqlInterface"
:jump-type-callback="jumpTypeCallback"
class="border-divider border-l-2 pl-4"
/>
</div>
</div>
<div v-if="children.length > 0" class="mb-2">
<h5 class="my-2">Children:</h5>
<GraphqlTypeLink
v-for="(child, index) in children"
:key="`child-${index}`"
:gql-type="child"
:jump-type-callback="jumpTypeCallback"
class="border-divider border-l-2 pl-4"
/>
</div>
<div v-if="gqlType.getFields">
<h5 class="my-2">Fields:</h5>
<GraphqlField
v-for="(field, index) in gqlType.getFields()"
:key="`field-${index}`"
class="border-divider border-l-2 pl-4"
:gql-field="field"
:is-highlighted="isFieldHighlighted({ field })"
:jump-type-callback="jumpTypeCallback"
/>
</div>
<div v-if="isEnum">
<h5 class="my-2">Values:</h5>
<div
v-for="(value, index) in gqlType.getValues()"
:key="`value-${index}`"
class="border-divider border-l-2 pl-4"
v-text="value.name"
></div>
</div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import {
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLInterfaceType,
} from "graphql"
export default defineComponent({
props: {
// eslint-disable-next-line vue/require-default-prop, vue/require-prop-types
gqlType: {},
gqlTypes: { type: Array, default: () => [] },
jumpTypeCallback: { type: Function, default: () => {} },
isHighlighted: { type: Boolean, default: false },
highlightedFields: { type: Array, default: () => [] },
},
computed: {
isInput() {
return this.gqlType instanceof GraphQLInputObjectType
},
isInterface() {
return this.gqlType instanceof GraphQLInterfaceType
},
isEnum() {
return this.gqlType instanceof GraphQLEnumType
},
interfaces() {
return (this.gqlType.getInterfaces && this.gqlType.getInterfaces()) || []
},
children() {
return this.gqlTypes.filter(
(type) =>
type.getInterfaces && type.getInterfaces().includes(this.gqlType)
)
},
},
methods: {
isFieldHighlighted({ field }) {
return !!this.highlightedFields.find(({ name }) => name === field.name)
},
},
})
</script>
<style scoped lang="scss">
.type-highlighted {
@apply text-accent;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<span
:class="isScalar ? 'text-secondaryLight' : 'cursor-pointer text-accent'"
@click="jumpToType"
>
{{ typeString }}
</span>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import { GraphQLScalarType } from "graphql"
export default defineComponent({
props: {
// eslint-disable-next-line vue/require-default-prop
gqlType: null,
// (typeName: string) => void
// eslint-disable-next-line vue/require-default-prop
jumpTypeCallback: Function,
},
computed: {
typeString() {
return this.gqlType.toString()
},
isScalar() {
return this.resolveRootType(this.gqlType) instanceof GraphQLScalarType
},
},
methods: {
jumpToType() {
if (this.isScalar) return
this.jumpTypeCallback(this.gqlType)
},
resolveRootType(type) {
let t = type
while (t.ofType != null) t = t.ofType
return t
},
},
})
</script>

View File

@@ -0,0 +1,169 @@
import { shallowMount } from "@vue/test-utils"
import field from "../Field"
const gqlField = {
name: "testField",
args: [
{
name: "arg1",
type: "Arg1Type",
},
{
name: "arg2",
type: "Arg2type",
},
],
type: "FieldType",
description: "TestDescription",
}
const factory = (props) =>
shallowMount(field, {
propsData: props,
stubs: {
GraphqlTypeLink: {
template: "<span>Typelink</span>",
},
},
mocks: {
$t: (text) => text,
},
})
describe("field", () => {
test("mounts properly if props are given", () => {
const wrapper = factory({
gqlField,
})
expect(wrapper).toBeTruthy()
})
test("field title is set correctly for fields with no args", () => {
const wrapper = factory({
gqlField: {
...gqlField,
args: undefined,
},
})
expect(
wrapper
.find(".field-title")
.text()
.replace(/[\r\n]+/g, "")
.replace(/ +/g, " ")
).toEqual("testField : Typelink")
})
test("field title is correctly given for fields with single arg", () => {
const wrapper = factory({
gqlField: {
...gqlField,
args: [
{
name: "arg1",
type: "Arg1Type",
},
],
},
})
expect(
wrapper
.find(".field-title")
.text()
.replace(/[\r\n]+/g, "")
.replace(/ +/g, " ")
).toEqual("testField ( arg1: Typelink ) : Typelink")
})
test("field title is correctly given for fields with multiple args", () => {
const wrapper = factory({
gqlField: {
...gqlField,
args: [
{
name: "arg1",
type: "Arg1Type",
},
],
},
})
expect(
wrapper
.find(".field-title")
.text()
.replace(/[\r\n]+/g, "")
.replace(/ +/g, " ")
).toEqual("testField ( arg1: Typelink ) : Typelink")
})
test("all typelinks are passed the jump callback", () => {
const wrapper = factory({
gqlField: {
...gqlField,
args: [
{
name: "arg1",
type: "Arg1Type",
},
{
name: "arg2",
type: "Arg2Type",
},
],
},
})
expect(
wrapper
.find(".field-title")
.text()
.replace(/[\r\n]+/g, "")
.replace(/ +/g, " ")
).toEqual("testField ( arg1: Typelink , arg2: Typelink ) : Typelink")
})
test("description is rendered when it is present", () => {
const wrapper = factory({
gqlField,
})
expect(wrapper.find(".field-desc").text()).toEqual("TestDescription")
})
test("description not rendered when it is not present", () => {
const wrapper = factory({
gqlField: {
...gqlField,
description: undefined,
},
})
expect(wrapper.find(".field-desc").exists()).toEqual(false)
})
test("deprecation warning is displayed when field is deprecated", () => {
const wrapper = factory({
gqlField: {
...gqlField,
isDeprecated: true,
},
})
expect(wrapper.find(".field-deprecated").exists()).toEqual(true)
})
test("deprecation warning is not displayed wwhen field is not deprecated", () => {
const wrapper = factory({
gqlField: {
...gqlField,
isDeprecated: false,
},
})
expect(wrapper.find(".field-deprecated").exists()).toEqual(false)
})
})

View File

@@ -0,0 +1,241 @@
import { shallowMount } from "@vue/test-utils"
import {
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLObjectType,
} from "graphql"
import type from "../Type"
const gqlType = {
name: "TestType",
description: "TestDescription",
getFields: () => [{ name: "field1" }, { name: "field2" }],
}
const factory = (props) =>
shallowMount(type, {
mocks: {
$t: (text) => text,
},
propsData: { gqlTypes: [], ...props },
stubs: ["GraphqlField", "GraphqlTypeLink"],
})
describe("type", () => {
test("mounts properly when props are passed", () => {
const wrapper = factory({
gqlType,
})
expect(wrapper).toBeTruthy()
})
test("title of the type is rendered properly", () => {
const wrapper = factory({
gqlType,
})
expect(wrapper.find(".type-title").text()).toEqual("TestType")
})
test("description of the type is rendered properly if present", () => {
const wrapper = factory({
gqlType,
})
expect(wrapper.find(".type-desc").text()).toEqual("TestDescription")
})
test("description of the type is not rendered if not present", () => {
const wrapper = factory({
gqlType: {
...gqlType,
description: undefined,
},
})
expect(wrapper.find(".type-desc").exists()).toEqual(false)
})
test("fields are not rendered if not present", () => {
const wrapper = factory({
gqlType: {
...gqlType,
getFields: undefined,
},
})
expect(wrapper.find("GraphqlField-stub").exists()).toEqual(false)
})
test("all fields are rendered if present with props passed properly", () => {
const wrapper = factory({
gqlType: {
...gqlType,
},
})
expect(wrapper.findAll("GraphqlField-stub").length).toEqual(2)
})
test("prepends 'input' to type name for Input Types", () => {
const testType = new GraphQLInputObjectType({
name: "TestType",
fields: {},
})
const wrapper = factory({
gqlType: testType,
})
expect(wrapper.find(".type-title").text().startsWith("input")).toEqual(true)
})
test("prepends 'interface' to type name for Interface Types", () => {
const testType = new GraphQLInterfaceType({
name: "TestType",
fields: {},
})
const wrapper = factory({
gqlType: testType,
})
expect(wrapper.find(".type-title").text().startsWith("interface")).toEqual(
true
)
})
test("prepends 'enum' to type name for Enum Types", () => {
const testType = new GraphQLEnumType({
name: "TestType",
values: {},
})
const wrapper = factory({
gqlType: testType,
})
expect(wrapper.find(".type-title").text().startsWith("enum")).toEqual(true)
})
test("'interfaces' computed property returns all the related interfaces", () => {
const testInterfaceA = new GraphQLInterfaceType({
name: "TestInterfaceA",
fields: {},
})
const testInterfaceB = new GraphQLInterfaceType({
name: "TestInterfaceB",
fields: {},
})
const type = new GraphQLObjectType({
name: "TestType",
interfaces: [testInterfaceA, testInterfaceB],
fields: {},
})
const wrapper = factory({
gqlType: type,
})
expect(wrapper.vm.interfaces).toEqual(
expect.arrayContaining([testInterfaceA, testInterfaceB])
)
})
test("'interfaces' computed property returns an empty array if there are no interfaces", () => {
const type = new GraphQLObjectType({
name: "TestType",
fields: {},
})
const wrapper = factory({
gqlType: type,
})
expect(wrapper.vm.interfaces).toEqual([])
})
test("'interfaces' computed property returns an empty array if the type is an enum", () => {
const type = new GraphQLEnumType({
name: "TestType",
values: {},
})
const wrapper = factory({
gqlType: type,
})
expect(wrapper.vm.interfaces).toEqual([])
})
test("'children' computed property returns all the types implementing an interface", () => {
const testInterface = new GraphQLInterfaceType({
name: "TestInterface",
fields: {},
})
const typeA = new GraphQLObjectType({
name: "TypeA",
interfaces: [testInterface],
fields: {},
})
const typeB = new GraphQLObjectType({
name: "TypeB",
interfaces: [testInterface],
fields: {},
})
const wrapper = factory({
gqlType: testInterface,
gqlTypes: [testInterface, typeA, typeB],
})
expect(wrapper.vm.children).toEqual(expect.arrayContaining([typeA, typeB]))
})
test("'children' computed property returns an empty array if there are no types implementing the interface", () => {
const testInterface = new GraphQLInterfaceType({
name: "TestInterface",
fields: {},
})
const typeA = new GraphQLObjectType({
name: "TypeA",
fields: {},
})
const typeB = new GraphQLObjectType({
name: "TypeB",
fields: {},
})
const wrapper = factory({
gqlType: testInterface,
gqlTypes: [testInterface, typeA, typeB],
})
expect(wrapper.vm.children).toEqual([])
})
test("'children' computed property returns an empty array if the type is an enum", () => {
const testInterface = new GraphQLInterfaceType({
name: "TestInterface",
fields: {},
})
const testType = new GraphQLEnumType({
name: "TestEnum",
values: {},
})
const wrapper = factory({
gqlType: testType,
gqlTypes: [testInterface, testType],
})
expect(wrapper.vm.children).toEqual([])
})
})

View File

@@ -0,0 +1,58 @@
import { shallowMount } from "@vue/test-utils"
import { GraphQLInt } from "graphql"
import typelink from "../TypeLink.vue"
const factory = (props) =>
shallowMount(typelink, {
propsData: props,
})
const gqlType = {
toString: () => "TestType",
}
describe("typelink", () => {
test("mounts properly when valid props are given", () => {
const wrapper = factory({
gqlType,
jumpTypeCallback: jest.fn(),
})
expect(wrapper).toBeTruthy()
})
test("jumpToTypeCallback is called when the link is clicked", async () => {
const callback = jest.fn()
const wrapper = factory({
gqlType,
jumpTypeCallback: callback,
})
await wrapper.trigger("click")
expect(callback).toHaveBeenCalledTimes(1)
})
test("jumpToType callback is not called if the root type is a scalar", async () => {
const callback = jest.fn()
const wrapper = factory({
gqlType: GraphQLInt,
jumpTypeCallback: callback,
})
await wrapper.trigger("click")
expect(callback).not.toHaveBeenCalled()
})
test("link text is the type string", () => {
const wrapper = factory({
gqlType,
jumpTypeCallback: jest.fn(),
})
expect(wrapper.text()).toBe("TestType")
})
})