diff options
Diffstat (limited to 'spec/frontend/packages_and_registries/package_registry')
27 files changed, 3073 insertions, 16 deletions
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap new file mode 100644 index 00000000000..e9f80d5f512 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConanInstallation renders all the messages 1`] = ` +<div> + <installation-title-stub + options="[object Object]" + packagetype="conan" + /> + + <code-instruction-stub + copytext="Copy Conan Command" + instruction="conan install @gitlab-org/package-15 --remote=gitlab" + label="Conan Command" + trackingaction="copy_conan_command" + trackinglabel="code_instruction" + /> + + <h3 + class="gl-font-lg" + > + Registry setup + </h3> + + <code-instruction-stub + copytext="Copy Conan Setup Command" + instruction="conan remote add gitlab conanPath" + label="Add Conan Remote" + trackingaction="copy_conan_setup_command" + trackinglabel="code_instruction" + /> + + <gl-sprintf-stub + message="For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}." + /> +</div> +`; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap new file mode 100644 index 00000000000..f83df7b11f4 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DependencyRow renders full dependency 1`] = ` +<div + class="gl-responsive-table-row" +> + <div + class="table-section section-50" + > + <strong + class="gl-text-body" + > + Ninject.Extensions.Factory + </strong> + + <span + data-testid="target-framework" + > + + (.NETCoreApp3.1) + + </span> + </div> + + <div + class="table-section section-50 gl-display-flex gl-md-justify-content-end" + data-testid="version-pattern" + > + <span + class="gl-text-body" + > + 3.3.2 + </span> + </div> +</div> +`; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap new file mode 100644 index 00000000000..881d441e116 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FileSha renders 1`] = ` +<div + class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all gl-py-2 gl-border-b-solid gl-border-gray-100 gl-border-b-1" +> + <!----> + + <span> + <div + class="gl-px-4" + > + + bar: + foo + + <gl-button-stub + aria-label="Copy this value" + buttontextclasses="" + category="tertiary" + data-clipboard-text="foo" + icon="copy-to-clipboard" + size="small" + title="Copy SHA" + variant="default" + /> + </div> + </span> +</div> +`; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap new file mode 100644 index 00000000000..4865b8205ab --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap @@ -0,0 +1,135 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MavenInstallation groovy renders all the messages 1`] = ` +<div> + <installation-title-stub + options="[object Object],[object Object],[object Object]" + packagetype="maven" + /> + + <code-instruction-stub + class="gl-mb-5" + copytext="Copy Gradle Groovy DSL install command" + instruction="implementation 'appGroup:appName:appVersion'" + label="Gradle Groovy DSL install command" + trackingaction="copy_gradle_install_command" + trackinglabel="code_instruction" + /> + + <code-instruction-stub + copytext="Copy add Gradle Groovy DSL repository command" + instruction="maven { + url 'mavenPath' +}" + label="Add Gradle Groovy DSL repository command" + multiline="true" + trackingaction="copy_gradle_add_to_source_command" + trackinglabel="code_instruction" + /> +</div> +`; + +exports[`MavenInstallation kotlin renders all the messages 1`] = ` +<div> + <installation-title-stub + options="[object Object],[object Object],[object Object]" + packagetype="maven" + /> + + <code-instruction-stub + class="gl-mb-5" + copytext="Copy Gradle Kotlin DSL install command" + instruction="implementation(\\"appGroup:appName:appVersion\\")" + label="Gradle Kotlin DSL install command" + trackingaction="copy_kotlin_install_command" + trackinglabel="code_instruction" + /> + + <code-instruction-stub + copytext="Copy add Gradle Kotlin DSL repository command" + instruction="maven(\\"mavenPath\\")" + label="Add Gradle Kotlin DSL repository command" + multiline="true" + trackingaction="copy_kotlin_add_to_source_command" + trackinglabel="code_instruction" + /> +</div> +`; + +exports[`MavenInstallation maven renders all the messages 1`] = ` +<div> + <installation-title-stub + options="[object Object],[object Object],[object Object]" + packagetype="maven" + /> + + <p> + <gl-sprintf-stub + message="Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block." + /> + </p> + + <code-instruction-stub + copytext="Copy Maven XML" + instruction="<dependency> + <groupId>appGroup</groupId> + <artifactId>appName</artifactId> + <version>appVersion</version> +</dependency>" + label="" + multiline="true" + trackingaction="copy_maven_xml" + trackinglabel="code_instruction" + /> + + <code-instruction-stub + copytext="Copy Maven command" + instruction="mvn dependency:get -Dartifact=appGroup:appName:appVersion" + label="Maven Command" + trackingaction="copy_maven_command" + trackinglabel="code_instruction" + /> + + <h3 + class="gl-font-lg" + > + Registry setup + </h3> + + <p> + <gl-sprintf-stub + message="If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file." + /> + </p> + + <code-instruction-stub + copytext="Copy Maven registry XML" + instruction="<repositories> + <repository> + <id>gitlab-maven</id> + <url>mavenPath</url> + </repository> +</repositories> + +<distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>mavenPath</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>mavenPath</url> + </snapshotRepository> +</distributionManagement>" + label="" + multiline="true" + trackingaction="copy_maven_setup_xml" + trackinglabel="code_instruction" + /> + + <gl-sprintf-stub + message="For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}." + /> +</div> +`; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap new file mode 100644 index 00000000000..6a7f14dc33f --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NpmInstallation renders all the messages 1`] = ` +<div> + <installation-title-stub + options="[object Object],[object Object]" + packagetype="npm" + /> + + <code-instruction-stub + copytext="Copy npm command" + instruction="npm i @gitlab-org/package-15" + label="" + trackingaction="copy_npm_install_command" + trackinglabel="code_instruction" + /> + + <h3 + class="gl-font-lg" + > + Registry setup + </h3> + + <code-instruction-stub + copytext="Copy npm setup command" + instruction="echo @gitlab-org:registry=npmPath/ >> .npmrc" + label="" + trackingaction="copy_npm_setup_command" + trackinglabel="code_instruction" + /> + + <gl-sprintf-stub + message="You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more." + /> +</div> +`; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap new file mode 100644 index 00000000000..29ddd7b77ed --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NugetInstallation renders all the messages 1`] = ` +<div> + <installation-title-stub + options="[object Object]" + packagetype="nuget" + /> + + <code-instruction-stub + copytext="Copy NuGet Command" + instruction="nuget install @gitlab-org/package-15 -Source \\"GitLab\\"" + label="NuGet Command" + trackingaction="copy_nuget_install_command" + trackinglabel="code_instruction" + /> + + <h3 + class="gl-font-lg" + > + Registry setup + </h3> + + <code-instruction-stub + copytext="Copy NuGet Setup Command" + instruction="nuget source Add -Name \\"GitLab\\" -Source \\"nugetPath\\" -UserName <your_username> -Password <your_token>" + label="Add NuGet Source" + trackingaction="copy_nuget_setup_command" + trackinglabel="code_instruction" + /> + + <gl-sprintf-stub + message="For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}." + /> +</div> +`; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap new file mode 100644 index 00000000000..45d261625b4 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap @@ -0,0 +1,197 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PackageTitle renders with tags 1`] = ` +<div + class="gl-display-flex gl-flex-direction-column" + data-qa-selector="package_title" +> + <div + class="gl-display-flex gl-justify-content-space-between gl-py-3" + > + <div + class="gl-flex-direction-column gl-flex-grow-1" + > + <div + class="gl-display-flex" + > + <!----> + + <div + class="gl-display-flex gl-flex-direction-column" + > + <h1 + class="gl-font-size-h1 gl-mt-3 gl-mb-2" + data-testid="title" + > + @gitlab-org/package-15 + </h1> + + <div + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1" + > + <gl-icon-stub + class="gl-mr-3" + name="eye" + size="16" + /> + + <span + data-testid="sub-header" + > + v + 1.0.0 + published + <time-ago-tooltip-stub + class="gl-ml-2" + cssclass="" + time="2020-08-17T14:23:32Z" + tooltipplacement="top" + /> + </span> + </div> + </div> + </div> + + <div + class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3" + > + <div + class="gl-display-flex gl-align-items-center gl-mr-5" + > + <metadata-item-stub + data-testid="package-type" + icon="package" + link="" + size="s" + text="npm" + texttooltip="" + /> + </div> + <div + class="gl-display-flex gl-align-items-center gl-mr-5" + > + <metadata-item-stub + data-testid="package-size" + icon="disk" + link="" + size="s" + text="800.00 KiB" + texttooltip="" + /> + </div> + <div + class="gl-display-flex gl-align-items-center gl-mr-5" + > + <package-tags-stub + hidelabel="true" + tagdisplaylimit="2" + tags="[object Object],[object Object],[object Object]" + /> + </div> + </div> + </div> + + <!----> + </div> + + <p /> +</div> +`; + +exports[`PackageTitle renders without tags 1`] = ` +<div + class="gl-display-flex gl-flex-direction-column" + data-qa-selector="package_title" +> + <div + class="gl-display-flex gl-justify-content-space-between gl-py-3" + > + <div + class="gl-flex-direction-column gl-flex-grow-1" + > + <div + class="gl-display-flex" + > + <!----> + + <div + class="gl-display-flex gl-flex-direction-column" + > + <h1 + class="gl-font-size-h1 gl-mt-3 gl-mb-2" + data-testid="title" + > + @gitlab-org/package-15 + </h1> + + <div + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1" + > + <gl-icon-stub + class="gl-mr-3" + name="eye" + size="16" + /> + + <span + data-testid="sub-header" + > + v + 1.0.0 + published + <time-ago-tooltip-stub + class="gl-ml-2" + cssclass="" + time="2020-08-17T14:23:32Z" + tooltipplacement="top" + /> + </span> + </div> + </div> + </div> + + <div + class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3" + > + <div + class="gl-display-flex gl-align-items-center gl-mr-5" + > + <metadata-item-stub + data-testid="package-type" + icon="package" + link="" + size="s" + text="npm" + texttooltip="" + /> + </div> + <div + class="gl-display-flex gl-align-items-center gl-mr-5" + > + <metadata-item-stub + data-testid="package-size" + icon="disk" + link="" + size="s" + text="800.00 KiB" + texttooltip="" + /> + </div> + <div + class="gl-display-flex gl-align-items-center gl-mr-5" + > + <package-tags-stub + hidelabel="true" + tagdisplaylimit="2" + tags="[object Object],[object Object],[object Object]" + /> + </div> + </div> + </div> + + <!----> + </div> + + <p /> +</div> +`; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap new file mode 100644 index 00000000000..158bbbc3463 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PypiInstallation renders all the messages 1`] = ` +<div> + <installation-title-stub + options="[object Object]" + packagetype="pypi" + /> + + <code-instruction-stub + copytext="Copy Pip command" + data-testid="pip-command" + instruction="pip install @gitlab-org/package-15 --extra-index-url pypiPath" + label="Pip Command" + trackingaction="copy_pip_install_command" + trackinglabel="code_instruction" + /> + + <h3 + class="gl-font-lg" + > + Registry setup + </h3> + + <p> + <gl-sprintf-stub + message="If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file." + /> + </p> + + <code-instruction-stub + copytext="Copy .pypirc content" + data-testid="pypi-setup-content" + instruction="[gitlab] +repository = pypiSetupPath +username = __token__ +password = <your personal access token>" + label="" + multiline="true" + trackingaction="copy_pypi_setup_command" + trackinglabel="code_instruction" + /> + + <gl-sprintf-stub + message="For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}." + /> +</div> +`; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap new file mode 100644 index 00000000000..8f69f943112 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VersionRow renders 1`] = ` +<div + class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100" +> + <div + class="gl-display-flex gl-align-items-center gl-py-3 gl-px-5" + > + <!----> + + <div + class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1" + > + <div + class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1" + > + <div + class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0" + > + <div + class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0" + > + <gl-link-stub + class="gl-text-body gl-min-w-0" + href="243" + > + <span + class="gl-truncate" + title="@gitlab-org/package-15" + > + <span + class="gl-truncate-end" + > + @gitlab-org/package-15 + </span> + </span> + </gl-link-stub> + + <package-tags-stub + class="gl-ml-3" + hidelabel="true" + tagdisplaylimit="1" + tags="[object Object],[object Object],[object Object]" + /> + </div> + + <!----> + </div> + + <div + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1" + > + + 1.0.1 + + </div> + </div> + + <div + class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0" + > + <div + class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6" + > + <publish-method-stub + packageentity="[object Object]" + /> + </div> + + <div + class="gl-display-flex gl-align-items-center gl-min-h-6" + > + Created + <time-ago-tooltip-stub + cssclass="" + time="2021-08-10T09:33:54Z" + tooltipplacement="top" + /> + </div> + </div> + </div> + + <!----> + </div> + + <div + class="gl-display-flex" + > + <div + class="gl-w-7" + /> + + <!----> + + <div + class="gl-w-9" + /> + </div> +</div> +`; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js new file mode 100644 index 00000000000..0504a42dfcf --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js @@ -0,0 +1,130 @@ +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { + conanMetadata, + mavenMetadata, + nugetMetadata, + packageData, +} from 'jest/packages_and_registries/package_registry/mock_data'; +import component from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; +import { + PACKAGE_TYPE_NUGET, + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_NPM, +} from '~/packages_and_registries/package_registry/constants'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; + +const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata() }; +const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() }; +const nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() }; +const npmPackage = { packageType: PACKAGE_TYPE_NPM, metadata: {} }; + +describe('Package Additional Metadata', () => { + let wrapper; + const defaultProps = { + packageEntity: { + ...packageData(mavenPackage), + }, + }; + + const mountComponent = (props) => { + wrapper = shallowMountExtended(component, { + propsData: { ...defaultProps, ...props }, + stubs: { + DetailsRow, + GlSprintf, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findTitle = () => wrapper.findByTestId('title'); + const findMainArea = () => wrapper.findByTestId('main'); + const findNugetSource = () => wrapper.findByTestId('nuget-source'); + const findNugetLicense = () => wrapper.findByTestId('nuget-license'); + const findConanRecipe = () => wrapper.findByTestId('conan-recipe'); + const findMavenApp = () => wrapper.findByTestId('maven-app'); + const findMavenGroup = () => wrapper.findByTestId('maven-group'); + const findElementLink = (container) => container.findComponent(GlLink); + + it('has the correct title', () => { + mountComponent(); + + const title = findTitle(); + + expect(title.exists()).toBe(true); + expect(title.text()).toBe('Additional Metadata'); + }); + + it.each` + packageEntity | visible | packageType + ${mavenPackage} | ${true} | ${PACKAGE_TYPE_MAVEN} + ${conanPackage} | ${true} | ${PACKAGE_TYPE_CONAN} + ${nugetPackage} | ${true} | ${PACKAGE_TYPE_NUGET} + ${npmPackage} | ${false} | ${PACKAGE_TYPE_NPM} + `( + `It is $visible that the component is visible when the package is $packageType`, + ({ packageEntity, visible }) => { + mountComponent({ packageEntity }); + + expect(findTitle().exists()).toBe(visible); + expect(findMainArea().exists()).toBe(visible); + }, + ); + + describe('nuget metadata', () => { + beforeEach(() => { + mountComponent({ packageEntity: nugetPackage }); + }); + + it.each` + name | finderFunction | text | link | icon + ${'source'} | ${findNugetSource} | ${'Source project located at projectUrl'} | ${'projectUrl'} | ${'project'} + ${'license'} | ${findNugetLicense} | ${'License information located at licenseUrl'} | ${'licenseUrl'} | ${'license'} + `('$name element', ({ finderFunction, text, link, icon }) => { + const element = finderFunction(); + expect(element.exists()).toBe(true); + expect(element.text()).toBe(text); + expect(element.props('icon')).toBe(icon); + expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]); + }); + }); + + describe('conan metadata', () => { + beforeEach(() => { + mountComponent({ packageEntity: conanPackage }); + }); + + it.each` + name | finderFunction | text | icon + ${'recipe'} | ${findConanRecipe} | ${'Recipe: package-8/1.0.0@gitlab-org+gitlab-test/stable'} | ${'information-o'} + `('$name element', ({ finderFunction, text, icon }) => { + const element = finderFunction(); + expect(element.exists()).toBe(true); + expect(element.text()).toBe(text); + expect(element.props('icon')).toBe(icon); + }); + }); + + describe('maven metadata', () => { + beforeEach(() => { + mountComponent(); + }); + + it.each` + name | finderFunction | text | icon + ${'app'} | ${findMavenApp} | ${'App name: appName'} | ${'information-o'} + ${'group'} | ${findMavenGroup} | ${'App group: appGroup'} | ${'information-o'} + `('$name element', ({ finderFunction, text, icon }) => { + const element = finderFunction(); + expect(element.exists()).toBe(true); + expect(element.text()).toBe(text); + expect(element.props('icon')).toBe(icon); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js index 97444ec108f..5119512564f 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js @@ -1,35 +1,451 @@ -import { GlEmptyState } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlEmptyState, GlBadge, GlTabs, GlTab } from '@gitlab/ui'; +import { createLocalVue } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue'; +import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue'; +import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue'; +import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; +import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue'; +import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue'; +import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; +import { + FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, + DELETE_PACKAGE_ERROR_MESSAGE, + PACKAGE_TYPE_COMPOSER, + DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + DELETE_PACKAGE_FILE_ERROR_MESSAGE, + PACKAGE_TYPE_NUGET, +} from '~/packages_and_registries/package_registry/constants'; + +import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql'; +import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; +import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; +import { + packageDetailsQuery, + packageData, + packageVersions, + dependencyLinks, + emptyPackageDetailsQuery, + packageDestroyMutation, + packageDestroyMutationError, + packageFiles, + packageDestroyFileMutation, + packageDestroyFileMutationError, +} from '../../mock_data'; + +jest.mock('~/flash'); +useMockLocationHelper(); + +const localVue = createLocalVue(); describe('PackagesApp', () => { let wrapper; + let apolloProvider; + + const provide = { + packageId: '111', + titleComponent: 'PackageTitle', + projectName: 'projectName', + canDelete: 'canDelete', + svgPath: 'svgPath', + npmPath: 'npmPath', + npmHelpPath: 'npmHelpPath', + projectListUrl: 'projectListUrl', + groupListUrl: 'groupListUrl', + }; - function createComponent() { - wrapper = shallowMount(PackagesApp, { - provide: { - titleComponent: 'titleComponent', - projectName: 'projectName', - canDelete: 'canDelete', - svgPath: 'svgPath', - npmPath: 'npmPath', - npmHelpPath: 'npmHelpPath', - projectListUrl: 'projectListUrl', - groupListUrl: 'groupListUrl', + function createComponent({ + resolver = jest.fn().mockResolvedValue(packageDetailsQuery()), + mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation()), + fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()), + } = {}) { + localVue.use(VueApollo); + + const requestHandlers = [ + [getPackageDetails, resolver], + [destroyPackageMutation, mutationResolver], + [destroyPackageFileMutation, fileDeleteMutationResolver], + ]; + apolloProvider = createMockApollo(requestHandlers); + + wrapper = shallowMountExtended(PackagesApp, { + localVue, + apolloProvider, + provide, + stubs: { + PackageTitle, + GlModal: { + template: '<div></div>', + methods: { + show: jest.fn(), + }, + }, + GlTabs, + GlTab, }, }); } - const emptyState = () => wrapper.findComponent(GlEmptyState); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findPackageTitle = () => wrapper.findComponent(PackageTitle); + const findPackageHistory = () => wrapper.findComponent(PackageHistory); + const findAdditionalMetadata = () => wrapper.findComponent(AdditionalMetadata); + const findInstallationCommands = () => wrapper.findComponent(InstallationCommands); + const findDeleteModal = () => wrapper.findByTestId('delete-modal'); + const findDeleteButton = () => wrapper.findByTestId('delete-package'); + const findPackageFiles = () => wrapper.findComponent(PackageFiles); + const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal'); + const findVersionRows = () => wrapper.findAllComponents(VersionRow); + const noVersionsMessage = () => wrapper.findByTestId('no-versions-message'); + const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge); + const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message'); + const findDependencyRows = () => wrapper.findAllComponents(DependencyRow); afterEach(() => { wrapper.destroy(); }); - it('renders an empty state component', () => { + it('renders an empty state component', async () => { + createComponent({ resolver: jest.fn().mockResolvedValue(emptyPackageDetailsQuery) }); + + await waitForPromises(); + + expect(findEmptyState().exists()).toBe(true); + }); + + it('renders the app and displays the package title', async () => { + createComponent(); + + await waitForPromises(); + + expect(findPackageTitle().exists()).toBe(true); + expect(findPackageTitle().props()).toMatchObject({ + packageEntity: expect.objectContaining(packageData()), + }); + }); + + it('emits an error message if the load fails', async () => { + createComponent({ resolver: jest.fn().mockRejectedValue() }); + + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, + }), + ); + }); + + it('renders history and has the right props', async () => { + createComponent(); + + await waitForPromises(); + + expect(findPackageHistory().exists()).toBe(true); + expect(findPackageHistory().props()).toMatchObject({ + packageEntity: expect.objectContaining(packageData()), + projectName: provide.projectName, + }); + }); + + it('renders additional metadata and has the right props', async () => { + createComponent(); + + await waitForPromises(); + + expect(findAdditionalMetadata().exists()).toBe(true); + expect(findAdditionalMetadata().props()).toMatchObject({ + packageEntity: expect.objectContaining(packageData()), + }); + }); + + it('renders installation commands and has the right props', async () => { createComponent(); - expect(emptyState().exists()).toBe(true); + await waitForPromises(); + + expect(findInstallationCommands().exists()).toBe(true); + expect(findInstallationCommands().props()).toMatchObject({ + packageEntity: expect.objectContaining(packageData()), + }); + }); + + describe('delete package', () => { + const originalReferrer = document.referrer; + const setReferrer = (value = provide.projectName) => { + Object.defineProperty(document, 'referrer', { + value, + configurable: true, + }); + }; + + const performDeletePackage = async () => { + await findDeleteButton().trigger('click'); + + findDeleteModal().vm.$emit('primary'); + + await waitForPromises(); + }; + + afterEach(() => { + Object.defineProperty(document, 'referrer', { + value: originalReferrer, + configurable: true, + }); + }); + + it('shows the delete confirmation modal when delete is clicked', async () => { + createComponent(); + + await waitForPromises(); + + await findDeleteButton().trigger('click'); + + expect(findDeleteModal().exists()).toBe(true); + }); + + describe('successful request', () => { + it('when referrer contains project name calls window.replace with project url', async () => { + setReferrer(); + + createComponent(); + + await waitForPromises(); + + await performDeletePackage(); + + expect(window.location.replace).toHaveBeenCalledWith( + 'projectListUrl?showSuccessDeleteAlert=true', + ); + }); + + it('when referrer does not contain project name calls window.replace with group url', async () => { + setReferrer('baz'); + + createComponent(); + + await waitForPromises(); + + await performDeletePackage(); + + expect(window.location.replace).toHaveBeenCalledWith( + 'groupListUrl?showSuccessDeleteAlert=true', + ); + }); + }); + + describe('request failure', () => { + it('on global failure it displays an alert', async () => { + createComponent({ mutationResolver: jest.fn().mockRejectedValue() }); + + await waitForPromises(); + + await performDeletePackage(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + }), + ); + }); + + it('on payload with error it displays an alert', async () => { + createComponent({ + mutationResolver: jest.fn().mockResolvedValue(packageDestroyMutationError()), + }); + + await waitForPromises(); + + await performDeletePackage(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + }), + ); + }); + }); + }); + + describe('package files', () => { + it('renders the package files component and has the right props', async () => { + const expectedFile = { ...packageFiles()[0] }; + // eslint-disable-next-line no-underscore-dangle + delete expectedFile.__typename; + createComponent(); + + await waitForPromises(); + + expect(findPackageFiles().exists()).toBe(true); + + expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile); + }); + + it('does not render the package files table when the package is composer', async () => { + createComponent({ + resolver: jest + .fn() + .mockResolvedValue(packageDetailsQuery({ packageType: PACKAGE_TYPE_COMPOSER })), + }); + + await waitForPromises(); + + expect(findPackageFiles().exists()).toBe(false); + }); + + describe('deleting a file', () => { + const [fileToDelete] = packageFiles(); + + const doDeleteFile = () => { + findPackageFiles().vm.$emit('delete-file', fileToDelete); + + findDeleteFileModal().vm.$emit('primary'); + + return waitForPromises(); + }; + + it('opens a confirmation modal', async () => { + createComponent(); + + await waitForPromises(); + + findPackageFiles().vm.$emit('delete-file', fileToDelete); + + await nextTick(); + + expect(findDeleteFileModal().exists()).toBe(true); + }); + + it('confirming on the modal deletes the file and shows a success message', async () => { + const resolver = jest.fn().mockResolvedValue(packageDetailsQuery()); + createComponent({ resolver }); + + await waitForPromises(); + + await doDeleteFile(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + }), + ); + // we are re-fetching the package details, so we expect the resolver to have been called twice + expect(resolver).toHaveBeenCalledTimes(2); + }); + + describe('errors', () => { + it('shows an error when the mutation request fails', async () => { + createComponent({ fileDeleteMutationResolver: jest.fn().mockRejectedValue() }); + await waitForPromises(); + + await doDeleteFile(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + }), + ); + }); + + it('shows an error when the mutation request returns an error payload', async () => { + createComponent({ + fileDeleteMutationResolver: jest + .fn() + .mockResolvedValue(packageDestroyFileMutationError()), + }); + await waitForPromises(); + + await doDeleteFile(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + }), + ); + }); + }); + }); + }); + + describe('versions', () => { + it('displays the correct version count when the package has versions', async () => { + createComponent(); + + await waitForPromises(); + + expect(findVersionRows()).toHaveLength(packageVersions().length); + }); + + it('binds the correct props', async () => { + const [versionPackage] = packageVersions(); + // eslint-disable-next-line no-underscore-dangle + delete versionPackage.__typename; + delete versionPackage.tags; + + createComponent(); + + await waitForPromises(); + + expect(findVersionRows().at(0).props()).toMatchObject({ + packageEntity: expect.objectContaining(versionPackage), + }); + }); + + it('displays the no versions message when there are none', async () => { + createComponent({ + resolver: jest.fn().mockResolvedValue(packageDetailsQuery({ versions: { nodes: [] } })), + }); + + await waitForPromises(); + + expect(noVersionsMessage().exists()).toBe(true); + }); + }); + describe('dependency links', () => { + it('does not show the dependency links for a non nuget package', async () => { + createComponent(); + + expect(findDependenciesCountBadge().exists()).toBe(false); + }); + + it('shows the dependencies tab with 0 count when a nuget package with no dependencies', async () => { + createComponent({ + resolver: jest.fn().mockResolvedValue( + packageDetailsQuery({ + packageType: PACKAGE_TYPE_NUGET, + dependencyLinks: { nodes: [] }, + }), + ), + }); + + await waitForPromises(); + + expect(findDependenciesCountBadge().exists()).toBe(true); + expect(findDependenciesCountBadge().text()).toBe('0'); + expect(findNoDependenciesMessage().exists()).toBe(true); + }); + + it('renders the correct number of dependency rows for a nuget package', async () => { + createComponent({ + resolver: jest.fn().mockResolvedValue( + packageDetailsQuery({ + packageType: PACKAGE_TYPE_NUGET, + }), + ), + }); + await waitForPromises(); + + expect(findDependenciesCountBadge().exists()).toBe(true); + expect(findDependenciesCountBadge().text()).toBe(dependencyLinks().length.toString()); + expect(findDependencyRows()).toHaveLength(dependencyLinks().length); + }); }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js new file mode 100644 index 00000000000..aedf20e873a --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js @@ -0,0 +1,118 @@ +import { GlSprintf, GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { packageData } from 'jest/packages_and_registries/package_registry/mock_data'; +import ComposerInstallation from '~/packages_and_registries/package_registry/components/details/composer_installation.vue'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import { + TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND, + TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND, + PACKAGE_TYPE_COMPOSER, +} from '~/packages_and_registries/package_registry/constants'; + +const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_COMPOSER }; + +describe('ComposerInstallation', () => { + let wrapper; + + const findRootNode = () => wrapper.findByTestId('root-node'); + const findRegistryInclude = () => wrapper.findByTestId('registry-include'); + const findPackageInclude = () => wrapper.findByTestId('package-include'); + const findHelpText = () => wrapper.findByTestId('help-text'); + const findHelpLink = () => wrapper.findComponent(GlLink); + const findInstallationTitle = () => wrapper.findComponent(InstallationTitle); + + function createComponent(groupListUrl = 'groupListUrl') { + wrapper = shallowMountExtended(ComposerInstallation, { + provide: { + composerHelpPath: 'composerHelpPath', + composerConfigRepositoryName: 'composerConfigRepositoryName', + composerPath: 'composerPath', + groupListUrl, + }, + propsData: { packageEntity }, + stubs: { + GlSprintf, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('install command switch', () => { + it('has the installation title component', () => { + createComponent(); + + expect(findInstallationTitle().exists()).toBe(true); + expect(findInstallationTitle().props()).toMatchObject({ + packageType: 'composer', + options: [{ value: 'composer', label: 'Show Composer commands' }], + }); + }); + }); + + describe('registry include command', () => { + beforeEach(() => { + createComponent(); + }); + + it('uses code_instructions', () => { + const registryIncludeCommand = findRegistryInclude(); + expect(registryIncludeCommand.exists()).toBe(true); + expect(registryIncludeCommand.props()).toMatchObject({ + instruction: `composer config repositories.composerConfigRepositoryName '{"type": "composer", "url": "composerPath"}'`, + copyText: 'Copy registry include', + trackingAction: TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND, + }); + }); + + it('has the correct title', () => { + expect(findRegistryInclude().props('label')).toBe('Add composer registry'); + }); + }); + + describe('package include command', () => { + beforeEach(() => { + createComponent(); + }); + + it('uses code_instructions', () => { + const registryIncludeCommand = findPackageInclude(); + expect(registryIncludeCommand.exists()).toBe(true); + expect(registryIncludeCommand.props()).toMatchObject({ + instruction: 'composer req @gitlab-org/package-15:1.0.0', + copyText: 'Copy require package include', + trackingAction: TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND, + }); + }); + + it('has the correct title', () => { + expect(findPackageInclude().props('label')).toBe('Install package version'); + }); + + it('has the correct help text', () => { + expect(findHelpText().text()).toBe( + 'For more information on Composer packages in GitLab, see the documentation.', + ); + expect(findHelpLink().attributes()).toMatchObject({ + href: 'composerHelpPath', + target: '_blank', + }); + }); + }); + + describe('root node', () => { + it('is normally rendered', () => { + createComponent(); + + expect(findRootNode().exists()).toBe(true); + }); + + it('is not rendered when the group does not exist', () => { + createComponent(''); + + expect(findRootNode().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js new file mode 100644 index 00000000000..6b642cc21b7 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js @@ -0,0 +1,65 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { packageData } from 'jest/packages_and_registries/package_registry/mock_data'; +import ConanInstallation from '~/packages_and_registries/package_registry/components/details/conan_installation.vue'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import { PACKAGE_TYPE_CONAN } from '~/packages_and_registries/package_registry/constants'; +import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue'; + +const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_CONAN }; + +describe('ConanInstallation', () => { + let wrapper; + + const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions); + const findInstallationTitle = () => wrapper.findComponent(InstallationTitle); + + function createComponent() { + wrapper = shallowMountExtended(ConanInstallation, { + provide: { + conanHelpPath: 'conanHelpPath', + conanPath: 'conanPath', + }, + propsData: { + packageEntity, + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders all the messages', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('install command switch', () => { + it('has the installation title component', () => { + expect(findInstallationTitle().exists()).toBe(true); + expect(findInstallationTitle().props()).toMatchObject({ + packageType: 'conan', + options: [{ value: 'conan', label: 'Show Conan commands' }], + }); + }); + }); + + describe('installation commands', () => { + it('renders the correct command', () => { + expect(findCodeInstructions().at(0).props('instruction')).toBe( + 'conan install @gitlab-org/package-15 --remote=gitlab', + ); + }); + }); + + describe('setup commands', () => { + it('renders the correct command', () => { + expect(findCodeInstructions().at(1).props('instruction')).toBe( + 'conan remote add gitlab conanPath', + ); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js new file mode 100644 index 00000000000..9aed5b90c73 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js @@ -0,0 +1,69 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue'; +import { dependencyLinks } from '../../mock_data'; + +describe('DependencyRow', () => { + let wrapper; + + const [fullDependencyLink] = dependencyLinks(); + const { dependency, metadata } = fullDependencyLink; + + function createComponent(dependencyLink = fullDependencyLink) { + wrapper = shallowMountExtended(DependencyRow, { + propsData: { + dependencyLink, + }, + }); + } + + const dependencyVersion = () => wrapper.findByTestId('version-pattern'); + const dependencyFramework = () => wrapper.findByTestId('target-framework'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('renders', () => { + it('full dependency', () => { + createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('version', () => { + it('does not render any version information when not supplied', () => { + createComponent({ + ...fullDependencyLink, + dependency: { ...dependency, versionPattern: undefined }, + }); + + expect(dependencyVersion().exists()).toBe(false); + }); + + it('does render version info when it exists', () => { + createComponent(); + + expect(dependencyVersion().exists()).toBe(true); + expect(dependencyVersion().text()).toBe(dependency.versionPattern); + }); + }); + + describe('target framework', () => { + it('does not render any framework information when not supplied', () => { + createComponent({ + ...fullDependencyLink, + metadata: { ...metadata, targetFramework: undefined }, + }); + + expect(dependencyFramework().exists()).toBe(false); + }); + + it('does render framework info when it exists', () => { + createComponent(); + + expect(dependencyFramework().exists()).toBe(true); + expect(dependencyFramework().text()).toBe(`(${metadata.targetFramework})`); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js new file mode 100644 index 00000000000..ebfbbe5b864 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js @@ -0,0 +1,33 @@ +import { shallowMount } from '@vue/test-utils'; + +import FileSha from '~/packages_and_registries/package_registry/components/details/file_sha.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; + +describe('FileSha', () => { + let wrapper; + + const defaultProps = { sha: 'foo', title: 'bar' }; + + function createComponent() { + wrapper = shallowMount(FileSha, { + propsData: { + ...defaultProps, + }, + stubs: { + ClipboardButton, + DetailsRow, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders', () => { + createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js new file mode 100644 index 00000000000..5fe795f768e --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js @@ -0,0 +1,58 @@ +import { shallowMount } from '@vue/test-utils'; + +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import PersistedDropdownSelection from '~/vue_shared/components/registry/persisted_dropdown_selection.vue'; + +describe('InstallationTitle', () => { + let wrapper; + + const defaultProps = { packageType: 'foo', options: [{ value: 'foo', label: 'bar' }] }; + + const findPersistedDropdownSelection = () => wrapper.findComponent(PersistedDropdownSelection); + const findTitle = () => wrapper.find('h3'); + + function createComponent({ props = {} } = {}) { + wrapper = shallowMount(InstallationTitle, { + propsData: { + ...defaultProps, + ...props, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('has a title', () => { + createComponent(); + + expect(findTitle().exists()).toBe(true); + expect(findTitle().text()).toBe('Installation'); + }); + + describe('persisted dropdown selection', () => { + it('exists', () => { + createComponent(); + + expect(findPersistedDropdownSelection().exists()).toBe(true); + }); + + it('has the correct props', () => { + createComponent(); + + expect(findPersistedDropdownSelection().props()).toMatchObject({ + storageKey: 'package_foo_installation_instructions', + options: defaultProps.options, + }); + }); + + it('on change event emits a change event', () => { + createComponent(); + + findPersistedDropdownSelection().vm.$emit('change', 'baz'); + + expect(wrapper.emitted('change')).toEqual([['baz']]); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js new file mode 100644 index 00000000000..b24946c8638 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js @@ -0,0 +1,64 @@ +import { shallowMount } from '@vue/test-utils'; +import { packageData } from 'jest/packages_and_registries/package_registry/mock_data'; +import ComposerInstallation from '~/packages_and_registries/package_registry/components/details/composer_installation.vue'; +import ConanInstallation from '~/packages_and_registries/package_registry/components/details/conan_installation.vue'; +import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue'; + +import MavenInstallation from '~/packages_and_registries/package_registry/components/details/maven_installation.vue'; +import NpmInstallation from '~/packages_and_registries/package_registry/components/details/npm_installation.vue'; +import NugetInstallation from '~/packages_and_registries/package_registry/components/details/nuget_installation.vue'; +import PypiInstallation from '~/packages_and_registries/package_registry/components/details/pypi_installation.vue'; +import { + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_NPM, + PACKAGE_TYPE_NUGET, + PACKAGE_TYPE_PYPI, + PACKAGE_TYPE_COMPOSER, +} from '~/packages_and_registries/package_registry/constants'; + +const conanPackage = { ...packageData(), packageType: PACKAGE_TYPE_CONAN }; +const mavenPackage = { ...packageData(), packageType: PACKAGE_TYPE_MAVEN }; +const npmPackage = { ...packageData(), packageType: PACKAGE_TYPE_NPM }; +const nugetPackage = { ...packageData(), packageType: PACKAGE_TYPE_NUGET }; +const pypiPackage = { ...packageData(), packageType: PACKAGE_TYPE_PYPI }; +const composerPackage = { ...packageData(), packageType: PACKAGE_TYPE_COMPOSER }; + +describe('InstallationCommands', () => { + let wrapper; + + function createComponent(propsData) { + wrapper = shallowMount(InstallationCommands, { + propsData, + }); + } + + const npmInstallation = () => wrapper.find(NpmInstallation); + const mavenInstallation = () => wrapper.find(MavenInstallation); + const conanInstallation = () => wrapper.find(ConanInstallation); + const nugetInstallation = () => wrapper.find(NugetInstallation); + const pypiInstallation = () => wrapper.find(PypiInstallation); + const composerInstallation = () => wrapper.find(ComposerInstallation); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('installation instructions', () => { + describe.each` + packageEntity | selector + ${conanPackage} | ${conanInstallation} + ${mavenPackage} | ${mavenInstallation} + ${npmPackage} | ${npmInstallation} + ${nugetPackage} | ${nugetInstallation} + ${pypiPackage} | ${pypiInstallation} + ${composerPackage} | ${composerInstallation} + `('renders', ({ packageEntity, selector }) => { + it(`${packageEntity.packageType} instructions exist`, () => { + createComponent({ packageEntity }); + + expect(selector()).toExist(); + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js new file mode 100644 index 00000000000..eed7e903833 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js @@ -0,0 +1,213 @@ +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import { + packageData, + mavenMetadata, +} from 'jest/packages_and_registries/package_registry/mock_data'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import MavenInstallation from '~/packages_and_registries/package_registry/components/details/maven_installation.vue'; +import { + TRACKING_ACTION_COPY_MAVEN_XML, + TRACKING_ACTION_COPY_MAVEN_COMMAND, + TRACKING_ACTION_COPY_MAVEN_SETUP, + TRACKING_ACTION_COPY_GRADLE_INSTALL_COMMAND, + TRACKING_ACTION_COPY_GRADLE_ADD_TO_SOURCE_COMMAND, + TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND, + TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND, + PACKAGE_TYPE_MAVEN, +} from '~/packages_and_registries/package_registry/constants'; +import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue'; + +describe('MavenInstallation', () => { + let wrapper; + + const packageEntity = { + ...packageData(), + packageType: PACKAGE_TYPE_MAVEN, + metadata: mavenMetadata(), + }; + + const mavenHelpPath = 'mavenHelpPath'; + const mavenPath = 'mavenPath'; + + const xmlCodeBlock = `<dependency> + <groupId>appGroup</groupId> + <artifactId>appName</artifactId> + <version>appVersion</version> +</dependency>`; + const mavenCommandStr = 'mvn dependency:get -Dartifact=appGroup:appName:appVersion'; + const mavenSetupXml = `<repositories> + <repository> + <id>gitlab-maven</id> + <url>${mavenPath}</url> + </repository> +</repositories> + +<distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>${mavenPath}</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>${mavenPath}</url> + </snapshotRepository> +</distributionManagement>`; + const gradleGroovyInstallCommandText = `implementation 'appGroup:appName:appVersion'`; + const gradleGroovyAddSourceCommandText = `maven { + url '${mavenPath}' +}`; + const gradleKotlinInstallCommandText = `implementation("appGroup:appName:appVersion")`; + const gradleKotlinAddSourceCommandText = `maven("${mavenPath}")`; + + const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions); + const findInstallationTitle = () => wrapper.findComponent(InstallationTitle); + + function createComponent({ data = {} } = {}) { + wrapper = shallowMountExtended(MavenInstallation, { + provide: { + mavenHelpPath, + mavenPath, + }, + propsData: { + packageEntity, + }, + data() { + return data; + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('install command switch', () => { + it('has the installation title component', () => { + createComponent(); + + expect(findInstallationTitle().exists()).toBe(true); + expect(findInstallationTitle().props()).toMatchObject({ + packageType: 'maven', + options: [ + { value: 'maven', label: 'Maven XML' }, + { value: 'groovy', label: 'Gradle Groovy DSL' }, + { value: 'kotlin', label: 'Gradle Kotlin DSL' }, + ], + }); + }); + + it('on change event updates the instructions to show', async () => { + createComponent(); + + expect(findCodeInstructions().at(0).props('instruction')).toBe(xmlCodeBlock); + findInstallationTitle().vm.$emit('change', 'groovy'); + + await nextTick(); + + expect(findCodeInstructions().at(0).props('instruction')).toBe( + gradleGroovyInstallCommandText, + ); + }); + }); + + describe('maven', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders all the messages', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('installation commands', () => { + it('renders the correct xml block', () => { + expect(findCodeInstructions().at(0).props()).toMatchObject({ + instruction: xmlCodeBlock, + multiline: true, + trackingAction: TRACKING_ACTION_COPY_MAVEN_XML, + }); + }); + + it('renders the correct maven command', () => { + expect(findCodeInstructions().at(1).props()).toMatchObject({ + instruction: mavenCommandStr, + multiline: false, + trackingAction: TRACKING_ACTION_COPY_MAVEN_COMMAND, + }); + }); + }); + + describe('setup commands', () => { + it('renders the correct xml block', () => { + expect(findCodeInstructions().at(2).props()).toMatchObject({ + instruction: mavenSetupXml, + multiline: true, + trackingAction: TRACKING_ACTION_COPY_MAVEN_SETUP, + }); + }); + }); + }); + + describe('groovy', () => { + beforeEach(() => { + createComponent({ data: { instructionType: 'groovy' } }); + }); + + it('renders all the messages', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('installation commands', () => { + it('renders the gradle install command', () => { + expect(findCodeInstructions().at(0).props()).toMatchObject({ + instruction: gradleGroovyInstallCommandText, + multiline: false, + trackingAction: TRACKING_ACTION_COPY_GRADLE_INSTALL_COMMAND, + }); + }); + }); + + describe('setup commands', () => { + it('renders the correct gradle command', () => { + expect(findCodeInstructions().at(1).props()).toMatchObject({ + instruction: gradleGroovyAddSourceCommandText, + multiline: true, + trackingAction: TRACKING_ACTION_COPY_GRADLE_ADD_TO_SOURCE_COMMAND, + }); + }); + }); + }); + + describe('kotlin', () => { + beforeEach(() => { + createComponent({ data: { instructionType: 'kotlin' } }); + }); + + it('renders all the messages', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('installation commands', () => { + it('renders the gradle install command', () => { + expect(findCodeInstructions().at(0).props()).toMatchObject({ + instruction: gradleKotlinInstallCommandText, + multiline: false, + trackingAction: TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND, + }); + }); + }); + + describe('setup commands', () => { + it('renders the correct gradle command', () => { + expect(findCodeInstructions().at(1).props()).toMatchObject({ + instruction: gradleKotlinAddSourceCommandText, + multiline: true, + trackingAction: TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND, + }); + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js new file mode 100644 index 00000000000..083c6858ad0 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js @@ -0,0 +1,122 @@ +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import { packageData } from 'jest/packages_and_registries/package_registry/mock_data'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import NpmInstallation from '~/packages_and_registries/package_registry/components/details/npm_installation.vue'; +import { + TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND, + TRACKING_ACTION_COPY_NPM_SETUP_COMMAND, + TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND, + TRACKING_ACTION_COPY_YARN_SETUP_COMMAND, + PACKAGE_TYPE_NPM, + NPM_PACKAGE_MANAGER, + YARN_PACKAGE_MANAGER, +} from '~/packages_and_registries/package_registry/constants'; +import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue'; + +const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_NPM }; + +describe('NpmInstallation', () => { + let wrapper; + + const npmInstallationCommandLabel = 'npm i @gitlab-org/package-15'; + const yarnInstallationCommandLabel = 'yarn add @gitlab-org/package-15'; + + const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions); + const findInstallationTitle = () => wrapper.findComponent(InstallationTitle); + + function createComponent({ data = {} } = {}) { + wrapper = shallowMountExtended(NpmInstallation, { + provide: { + npmHelpPath: 'npmHelpPath', + npmPath: 'npmPath', + }, + propsData: { + packageEntity, + }, + data() { + return data; + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders all the messages', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('install command switch', () => { + it('has the installation title component', () => { + expect(findInstallationTitle().exists()).toBe(true); + expect(findInstallationTitle().props()).toMatchObject({ + packageType: NPM_PACKAGE_MANAGER, + options: [ + { value: NPM_PACKAGE_MANAGER, label: 'Show NPM commands' }, + { value: YARN_PACKAGE_MANAGER, label: 'Show Yarn commands' }, + ], + }); + }); + + it('on change event updates the instructions to show', async () => { + createComponent(); + + expect(findCodeInstructions().at(0).props('instruction')).toBe(npmInstallationCommandLabel); + findInstallationTitle().vm.$emit('change', YARN_PACKAGE_MANAGER); + + await nextTick(); + + expect(findCodeInstructions().at(0).props('instruction')).toBe(yarnInstallationCommandLabel); + }); + }); + + describe('npm', () => { + beforeEach(() => { + createComponent(); + }); + it('renders the correct installation command', () => { + expect(findCodeInstructions().at(0).props()).toMatchObject({ + instruction: npmInstallationCommandLabel, + multiline: false, + trackingAction: TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND, + }); + }); + + it('renders the correct setup command', () => { + expect(findCodeInstructions().at(1).props()).toMatchObject({ + instruction: 'echo @gitlab-org:registry=npmPath/ >> .npmrc', + multiline: false, + trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND, + }); + }); + }); + + describe('yarn', () => { + beforeEach(() => { + createComponent({ data: { instructionType: YARN_PACKAGE_MANAGER } }); + }); + + it('renders the correct setup command', () => { + expect(findCodeInstructions().at(0).props()).toMatchObject({ + instruction: yarnInstallationCommandLabel, + multiline: false, + trackingAction: TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND, + }); + }); + + it('renders the correct registry command', () => { + expect(findCodeInstructions().at(1).props()).toMatchObject({ + instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc', + multiline: false, + trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND, + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js new file mode 100644 index 00000000000..c48a3f07299 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js @@ -0,0 +1,75 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { packageData } from 'jest/packages_and_registries/package_registry/mock_data'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import NugetInstallation from '~/packages_and_registries/package_registry/components/details/nuget_installation.vue'; +import { + TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND, + TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND, + PACKAGE_TYPE_NUGET, +} from '~/packages_and_registries/package_registry/constants'; +import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue'; + +const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_NUGET }; + +describe('NugetInstallation', () => { + let wrapper; + + const nugetInstallationCommandStr = 'nuget install @gitlab-org/package-15 -Source "GitLab"'; + const nugetSetupCommandStr = + 'nuget source Add -Name "GitLab" -Source "nugetPath" -UserName <your_username> -Password <your_token>'; + + const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions); + const findInstallationTitle = () => wrapper.findComponent(InstallationTitle); + + function createComponent() { + wrapper = shallowMountExtended(NugetInstallation, { + provide: { + nugetHelpPath: 'nugetHelpPath', + nugetPath: 'nugetPath', + }, + propsData: { + packageEntity, + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders all the messages', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('install command switch', () => { + it('has the installation title component', () => { + expect(findInstallationTitle().exists()).toBe(true); + expect(findInstallationTitle().props()).toMatchObject({ + packageType: 'nuget', + options: [{ value: 'nuget', label: 'Show Nuget commands' }], + }); + }); + }); + + describe('installation commands', () => { + it('renders the correct command', () => { + expect(findCodeInstructions().at(0).props()).toMatchObject({ + instruction: nugetInstallationCommandStr, + trackingAction: TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND, + }); + }); + }); + + describe('setup commands', () => { + it('renders the correct command', () => { + expect(findCodeInstructions().at(1).props()).toMatchObject({ + instruction: nugetSetupCommandStr, + trackingAction: TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND, + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js new file mode 100644 index 00000000000..042b2026199 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js @@ -0,0 +1,272 @@ +import { GlDropdown, GlButton } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import stubChildren from 'helpers/stub_children'; +import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { packageFiles as packageFilesMock } from 'jest/packages_and_registries/package_registry/mock_data'; +import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +describe('Package Files', () => { + let wrapper; + + const findAllRows = () => wrapper.findAllByTestId('file-row'); + const findFirstRow = () => extendedWrapper(findAllRows().at(0)); + const findSecondRow = () => extendedWrapper(findAllRows().at(1)); + const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link'); + const findFirstRowCommitLink = () => findFirstRow().findByTestId('commit-link'); + const findSecondRowCommitLink = () => findSecondRow().findByTestId('commit-link'); + const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon); + const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip); + const findFirstActionMenu = () => extendedWrapper(findFirstRow().findComponent(GlDropdown)); + const findActionMenuDelete = () => findFirstActionMenu().findByTestId('delete-file'); + const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton); + const findFirstRowShaComponent = (id) => wrapper.findByTestId(id); + + const files = packageFilesMock(); + const [file] = files; + + const createComponent = ({ packageFiles = [file], canDelete = true } = {}) => { + wrapper = mountExtended(PackageFiles, { + provide: { canDelete }, + propsData: { + packageFiles, + }, + stubs: { + ...stubChildren(PackageFiles), + GlTable: false, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('rows', () => { + it('renders a single file for an npm package', () => { + createComponent(); + + expect(findAllRows()).toHaveLength(1); + }); + + it('renders multiple files for a package that contains more than one file', () => { + createComponent({ packageFiles: files }); + + expect(findAllRows()).toHaveLength(2); + }); + }); + + describe('link', () => { + it('exists', () => { + createComponent(); + + expect(findFirstRowDownloadLink().exists()).toBe(true); + }); + + it('has the correct attrs bound', () => { + createComponent(); + + expect(findFirstRowDownloadLink().attributes('href')).toBe(file.downloadPath); + }); + + it('emits "download-file" event on click', () => { + createComponent(); + + findFirstRowDownloadLink().vm.$emit('click'); + + expect(wrapper.emitted('download-file')).toEqual([[]]); + }); + }); + + describe('file-icon', () => { + it('exists', () => { + createComponent(); + + expect(findFirstRowFileIcon().exists()).toBe(true); + }); + + it('has the correct props bound', () => { + createComponent(); + + expect(findFirstRowFileIcon().props('fileName')).toBe(file.fileName); + }); + }); + + describe('time-ago tooltip', () => { + it('exists', () => { + createComponent(); + + expect(findFirstRowCreatedAt().exists()).toBe(true); + }); + + it('has the correct props bound', () => { + createComponent(); + + expect(findFirstRowCreatedAt().props('time')).toBe(file.createdAt); + }); + }); + + describe('commit', () => { + const withPipeline = { + ...file, + pipelines: [ + { + sha: 'sha', + id: 1, + commitPath: 'commitPath', + }, + ], + }; + + describe('when package file has a pipeline associated', () => { + it('exists', () => { + createComponent({ packageFiles: [withPipeline] }); + + expect(findFirstRowCommitLink().exists()).toBe(true); + }); + + it('the link points to the commit path', () => { + createComponent({ packageFiles: [withPipeline] }); + + expect(findFirstRowCommitLink().attributes('href')).toBe( + withPipeline.pipelines[0].commitPath, + ); + }); + + it('the text is the pipeline sha', () => { + createComponent({ packageFiles: [withPipeline] }); + + expect(findFirstRowCommitLink().text()).toBe(withPipeline.pipelines[0].sha); + }); + }); + + describe('when package file has no pipeline associated', () => { + it('does not exist', () => { + createComponent(); + + expect(findFirstRowCommitLink().exists()).toBe(false); + }); + }); + + describe('when only one file lacks an associated pipeline', () => { + it('renders the commit when it exists and not otherwise', () => { + createComponent({ packageFiles: [withPipeline, file] }); + + expect(findFirstRowCommitLink().exists()).toBe(true); + expect(findSecondRowCommitLink().exists()).toBe(false); + }); + }); + + describe('action menu', () => { + describe('when the user can delete', () => { + it('exists', () => { + createComponent(); + + expect(findFirstActionMenu().exists()).toBe(true); + }); + + describe('menu items', () => { + describe('delete file', () => { + it('exists', () => { + createComponent(); + + expect(findActionMenuDelete().exists()).toBe(true); + }); + + it('emits a delete event when clicked', () => { + createComponent(); + + findActionMenuDelete().vm.$emit('click'); + + const [[{ id }]] = wrapper.emitted('delete-file'); + expect(id).toBe(file.id); + }); + }); + }); + }); + + describe('when the user can not delete', () => { + const canDelete = false; + + it('does not exist', () => { + createComponent({ canDelete }); + + expect(findFirstActionMenu().exists()).toBe(false); + }); + }); + }); + }); + + describe('additional details', () => { + describe('details toggle button', () => { + it('exists', () => { + createComponent(); + + expect(findFirstToggleDetailsButton().exists()).toBe(true); + }); + + it('is hidden when no details is present', () => { + const { ...noShaFile } = file; + noShaFile.fileSha256 = null; + noShaFile.fileMd5 = null; + noShaFile.fileSha1 = null; + createComponent({ packageFiles: [noShaFile] }); + + expect(findFirstToggleDetailsButton().exists()).toBe(false); + }); + + it('toggles the details row', async () => { + createComponent(); + + expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down'); + + findFirstToggleDetailsButton().vm.$emit('click'); + await nextTick(); + + expect(findFirstRowShaComponent('sha-256').exists()).toBe(true); + expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-up'); + + findFirstToggleDetailsButton().vm.$emit('click'); + await nextTick(); + + expect(findFirstRowShaComponent('sha-256').exists()).toBe(false); + expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down'); + }); + }); + + describe('file shas', () => { + const showShaFiles = () => { + findFirstToggleDetailsButton().vm.$emit('click'); + return nextTick(); + }; + + it.each` + selector | title | sha + ${'sha-256'} | ${'SHA-256'} | ${'fileSha256'} + ${'md5'} | ${'MD5'} | ${'fileMd5'} + ${'sha-1'} | ${'SHA-1'} | ${'be93151dc23ac34a82752444556fe79b32c7a1ad'} + `('has a $title row', async ({ selector, title, sha }) => { + createComponent(); + + await showShaFiles(); + + expect(findFirstRowShaComponent(selector).props()).toMatchObject({ + title, + sha, + }); + }); + + it('does not display a row when the data is missing', async () => { + const { ...missingMd5 } = file; + missingMd5.fileMd5 = null; + + createComponent({ packageFiles: [missingMd5] }); + + await showShaFiles(); + + expect(findFirstRowShaComponent('md5').exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js new file mode 100644 index 00000000000..b69008f04f0 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js @@ -0,0 +1,122 @@ +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { stubComponent } from 'helpers/stub_component'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { + packageData, + packagePipelines, +} from 'jest/packages_and_registries/package_registry/mock_data'; +import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants'; +import component from '~/packages_and_registries/package_registry/components/details/package_history.vue'; +import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +describe('Package History', () => { + let wrapper; + const defaultProps = { + projectName: 'baz project', + packageEntity: { ...packageData() }, + }; + + const [onePipeline] = packagePipelines(); + + const createPipelines = (amount) => + [...Array(amount)].map((x, index) => packagePipelines({ id: index + 1 })[0]); + + const mountComponent = (props) => { + wrapper = shallowMountExtended(component, { + propsData: { ...defaultProps, ...props }, + stubs: { + HistoryItem: stubComponent(HistoryItem, { + template: '<div data-testid="history-element"><slot></slot></div>', + }), + GlSprintf, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findHistoryElement = (testId) => wrapper.findByTestId(testId); + const findElementLink = (container) => container.findComponent(GlLink); + const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip); + const findTitle = () => wrapper.findByTestId('title'); + const findTimeline = () => wrapper.findByTestId('timeline'); + + it('has the correct title', () => { + mountComponent(); + + const title = findTitle(); + + expect(title.exists()).toBe(true); + expect(title.text()).toBe('History'); + }); + + it('has a timeline container', () => { + mountComponent(); + + const title = findTimeline(); + + expect(title.exists()).toBe(true); + expect(title.classes()).toEqual( + expect.arrayContaining(['timeline', 'main-notes-list', 'notes']), + ); + }); + + describe.each` + name | amount | icon | text | timeAgoTooltip | link + ${'created-on'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'clock'} | ${'@gitlab-org/package-15 version 1.0.0 was first created'} | ${packageData().createdAt} | ${null} + ${'first-pipeline-commit'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'commit'} | ${'Created by commit #b83d6e39 on branch master'} | ${null} | ${onePipeline.commitPath} + ${'first-pipeline-pipeline'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pipeline'} | ${'Built by pipeline #1 triggered by Administrator'} | ${onePipeline.createdAt} | ${onePipeline.path} + ${'published'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'package'} | ${'Published to the baz project Package Registry'} | ${packageData().createdAt} | ${null} + ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'history'} | ${'Package has 1 archived update'} | ${null} | ${null} + ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 3} | ${'history'} | ${'Package has 2 archived updates'} | ${null} | ${null} + ${'pipeline-entry'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pencil'} | ${'Package updated by commit #b83d6e39 on branch master, built by pipeline #3, and published to the registry'} | ${packageData().createdAt} | ${onePipeline.commitPath} + `( + 'with $amount pipelines history element $name', + ({ name, icon, text, timeAgoTooltip, link, amount }) => { + let element; + + beforeEach(() => { + const packageEntity = { ...packageData(), pipelines: { nodes: createPipelines(amount) } }; + mountComponent({ + packageEntity, + }); + element = findHistoryElement(name); + }); + + it('exists', () => { + expect(element.exists()).toBe(true); + }); + + it('has the correct icon', () => { + expect(element.props('icon')).toBe(icon); + }); + + it('has the correct text', () => { + expect(element.text()).toBe(text); + }); + + it('time-ago tooltip', () => { + const timeAgo = findElementTimeAgo(element); + const exist = Boolean(timeAgoTooltip); + + expect(timeAgo.exists()).toBe(exist); + if (exist) { + expect(timeAgo.props('time')).toBe(timeAgoTooltip); + } + }); + + it('link', () => { + const linkElement = findElementLink(element); + const exist = Boolean(link); + + expect(linkElement.exists()).toBe(exist); + if (exist) { + expect(linkElement.attributes('href')).toBe(link); + } + }); + }, + ); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js new file mode 100644 index 00000000000..327f6d81905 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js @@ -0,0 +1,202 @@ +import { GlIcon, GlSprintf } from '@gitlab/ui'; +import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import PackageTags from '~/packages/shared/components/package_tags.vue'; +import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue'; +import { + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_NPM, + PACKAGE_TYPE_NUGET, +} from '~/packages_and_registries/package_registry/constants'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +import { packageData, packageFiles, packageTags, packagePipelines } from '../../mock_data'; + +const packageWithTags = { + ...packageData(), + tags: { nodes: packageTags() }, + packageFiles: { nodes: packageFiles() }, +}; + +describe('PackageTitle', () => { + let wrapper; + + function createComponent(packageEntity = packageWithTags) { + wrapper = shallowMountExtended(PackageTitle, { + propsData: { packageEntity }, + stubs: { + TitleArea, + GlSprintf, + }, + }); + return wrapper.vm.$nextTick(); + } + + const findTitleArea = () => wrapper.findComponent(TitleArea); + const findPackageType = () => wrapper.findByTestId('package-type'); + const findPackageSize = () => wrapper.findByTestId('package-size'); + const findPipelineProject = () => wrapper.findByTestId('pipeline-project'); + const findPackageRef = () => wrapper.findByTestId('package-ref'); + const findPackageTags = () => wrapper.findComponent(PackageTags); + const findPackageBadges = () => wrapper.findAllByTestId('tag-badge'); + const findSubHeaderIcon = () => wrapper.findComponent(GlIcon); + const findSubHeaderText = () => wrapper.findByTestId('sub-header'); + const findSubHeaderTimeAgo = () => wrapper.findComponent(TimeAgoTooltip); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('renders', () => { + it('without tags', async () => { + await createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('with tags', async () => { + await createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('with tags on mobile', async () => { + jest.spyOn(GlBreakpointInstance, 'isDesktop').mockReturnValue(false); + await createComponent(); + + await wrapper.vm.$nextTick(); + + expect(findPackageBadges()).toHaveLength(packageTags().length); + }); + }); + + describe('package title', () => { + it('is correctly bound', async () => { + await createComponent(); + + expect(findTitleArea().props('title')).toBe(packageData().name); + }); + }); + + describe('package icon', () => { + const iconUrl = 'a-fake-src'; + + it('shows an icon when present and package type is NUGET', async () => { + await createComponent({ + ...packageData(), + packageType: PACKAGE_TYPE_NUGET, + metadata: { iconUrl }, + }); + + expect(findTitleArea().props('avatar')).toBe(iconUrl); + }); + + it('hides the icon when not present', async () => { + await createComponent(); + + expect(findTitleArea().props('avatar')).toBe(null); + }); + }); + + describe('sub-header', () => { + it('has the eye icon', async () => { + await createComponent(); + + expect(findSubHeaderIcon().props('name')).toBe('eye'); + }); + + it('has a text showing version', async () => { + await createComponent(); + + expect(findSubHeaderText().text()).toMatchInterpolatedText('v 1.0.0 published'); + }); + + it('has a time ago tooltip component', async () => { + await createComponent(); + expect(findSubHeaderTimeAgo().props('time')).toBe(packageWithTags.createdAt); + }); + }); + + describe.each` + packageType | text + ${PACKAGE_TYPE_CONAN} | ${'Conan'} + ${PACKAGE_TYPE_MAVEN} | ${'Maven'} + ${PACKAGE_TYPE_NPM} | ${'npm'} + ${PACKAGE_TYPE_NUGET} | ${'NuGet'} + `(`package type`, ({ packageType, text }) => { + beforeEach(() => createComponent({ ...packageData, packageType })); + + it(`${packageType} should render ${text}`, () => { + expect(findPackageType().props()).toEqual(expect.objectContaining({ text, icon: 'package' })); + }); + }); + + describe('calculates the package size', () => { + it('correctly calculates when there is only 1 file', async () => { + await createComponent({ ...packageData(), packageFiles: { nodes: [packageFiles()[0]] } }); + + expect(findPackageSize().props()).toMatchObject({ text: '400.00 KiB', icon: 'disk' }); + }); + + it('correctly calculates when there are multiple files', async () => { + await createComponent(); + + expect(findPackageSize().props('text')).toBe('800.00 KiB'); + }); + }); + + describe('package tags', () => { + it('displays the package-tags component when the package has tags', async () => { + await createComponent(); + + expect(findPackageTags().exists()).toBe(true); + }); + + it('does not display the package-tags component when there are no tags', async () => { + await createComponent({ ...packageData(), tags: { nodes: [] } }); + + expect(findPackageTags().exists()).toBe(false); + }); + }); + + describe('package ref', () => { + it('does not display the ref if missing', async () => { + await createComponent(); + + expect(findPackageRef().exists()).toBe(false); + }); + + it('correctly shows the package ref if there is one', async () => { + await createComponent({ + ...packageData(), + pipelines: { nodes: packagePipelines({ ref: 'test' }) }, + }); + expect(findPackageRef().props()).toMatchObject({ + text: 'test', + icon: 'branch', + }); + }); + }); + + describe('pipeline project', () => { + it('does not display the project if missing', async () => { + await createComponent(); + + expect(findPipelineProject().exists()).toBe(false); + }); + + it('correctly shows the pipeline project if there is one', async () => { + await createComponent({ + ...packageData(), + pipelines: { nodes: packagePipelines() }, + }); + expect(findPipelineProject().props()).toMatchObject({ + text: packagePipelines()[0].project.name, + icon: 'review-list', + link: packagePipelines()[0].project.webUrl, + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js new file mode 100644 index 00000000000..410c1b65348 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js @@ -0,0 +1,80 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { packageData } from 'jest/packages_and_registries/package_registry/mock_data'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import PypiInstallation from '~/packages_and_registries/package_registry/components/details/pypi_installation.vue'; +import { + PACKAGE_TYPE_PYPI, + TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND, + TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND, +} from '~/packages_and_registries/package_registry/constants'; + +const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_PYPI }; + +describe('PypiInstallation', () => { + let wrapper; + + const pipCommandStr = 'pip install @gitlab-org/package-15 --extra-index-url pypiPath'; + const pypiSetupStr = `[gitlab] +repository = pypiSetupPath +username = __token__ +password = <your personal access token>`; + + const pipCommand = () => wrapper.findByTestId('pip-command'); + const setupInstruction = () => wrapper.findByTestId('pypi-setup-content'); + + const findInstallationTitle = () => wrapper.findComponent(InstallationTitle); + + function createComponent() { + wrapper = shallowMountExtended(PypiInstallation, { + provide: { + pypiHelpPath: 'pypiHelpPath', + pypiPath: 'pypiPath', + pypiSetupPath: 'pypiSetupPath', + }, + propsData: { + packageEntity, + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('install command switch', () => { + it('has the installation title component', () => { + expect(findInstallationTitle().exists()).toBe(true); + expect(findInstallationTitle().props()).toMatchObject({ + packageType: 'pypi', + options: [{ value: 'pypi', label: 'Show PyPi commands' }], + }); + }); + }); + + it('renders all the messages', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('installation commands', () => { + it('renders the correct pip command', () => { + expect(pipCommand().props()).toMatchObject({ + instruction: pipCommandStr, + trackingAction: TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND, + }); + }); + }); + + describe('setup commands', () => { + it('renders the correct setup block', () => { + expect(setupInstruction().props()).toMatchObject({ + instruction: pypiSetupStr, + multiline: true, + trackingAction: TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND, + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js new file mode 100644 index 00000000000..f7613949fe4 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js @@ -0,0 +1,89 @@ +import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import PackageTags from '~/packages/shared/components/package_tags.vue'; +import PublishMethod from '~/packages/shared/components/publish_method.vue'; +import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +import { packageVersions } from '../../mock_data'; + +const packageVersion = packageVersions()[0]; + +describe('VersionRow', () => { + let wrapper; + + const findListItem = () => wrapper.findComponent(ListItem); + const findLink = () => wrapper.findComponent(GlLink); + const findPackageTags = () => wrapper.findComponent(PackageTags); + const findPublishMethod = () => wrapper.findComponent(PublishMethod); + const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); + + function createComponent(packageEntity = packageVersion) { + wrapper = shallowMountExtended(VersionRow, { + propsData: { + packageEntity, + }, + stubs: { + ListItem, + GlSprintf, + GlTruncate, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders', () => { + createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('has a link to the version detail', () => { + createComponent(); + + expect(findLink().attributes('href')).toBe(`${getIdFromGraphQLId(packageVersion.id)}`); + expect(findLink().text()).toBe(packageVersion.name); + }); + + it('has the version of the package', () => { + createComponent(); + + expect(wrapper.text()).toContain(packageVersion.version); + }); + + it('has a package tags component', () => { + createComponent(); + + expect(findPackageTags().props('tags')).toBe(packageVersion.tags.nodes); + }); + + it('has a publish method component', () => { + createComponent(); + + expect(findPublishMethod().props('packageEntity')).toBe(packageVersion); + }); + it('has a time-ago tooltip', () => { + createComponent(); + + expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt); + }); + + describe('disabled status', () => { + it('disables the list item', () => { + createComponent({ ...packageVersion, status: 'something' }); + + expect(findListItem().props('disabled')).toBe(true); + }); + + it('disables the link', () => { + createComponent({ ...packageVersion, status: 'something' }); + + expect(findLink().attributes('disabled')).toBe('true'); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js new file mode 100644 index 00000000000..98ff29ef728 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -0,0 +1,251 @@ +export const packageTags = () => [ + { id: 'gid://gitlab/Packages::Tag/87', name: 'bananas_9', __typename: 'PackageTag' }, + { id: 'gid://gitlab/Packages::Tag/86', name: 'bananas_8', __typename: 'PackageTag' }, + { id: 'gid://gitlab/Packages::Tag/85', name: 'bananas_7', __typename: 'PackageTag' }, +]; + +export const packagePipelines = (extend) => [ + { + commitPath: '/namespace14/project14/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0', + createdAt: '2020-08-17T14:23:32Z', + id: 'gid://gitlab/Ci::Pipeline/36', + path: '/namespace14/project14/-/pipelines/36', + name: 'project14', + ref: 'master', + sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0', + project: { + name: 'project14', + webUrl: 'http://gdk.test:3000/namespace14/project14', + __typename: 'Project', + }, + user: { + name: 'Administrator', + }, + ...extend, + __typename: 'Pipeline', + }, +]; + +export const packageFiles = () => [ + { + id: 'gid://gitlab/Packages::PackageFile/118', + fileMd5: 'fileMd5', + fileName: 'foo-1.0.1.tgz', + fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ad', + fileSha256: 'fileSha256', + size: '409600', + createdAt: '2020-08-17T14:23:32Z', + downloadPath: 'downloadPath', + __typename: 'PackageFile', + }, + { + id: 'gid://gitlab/Packages::PackageFile/119', + fileMd5: null, + fileName: 'foo-1.0.2.tgz', + fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ss', + fileSha256: null, + size: '409600', + createdAt: '2020-08-17T14:23:32Z', + downloadPath: 'downloadPath', + __typename: 'PackageFile', + }, +]; + +export const dependencyLinks = () => [ + { + dependencyType: 'DEPENDENCIES', + id: 'gid://gitlab/Packages::DependencyLink/77', + __typename: 'PackageDependencyLink', + dependency: { + id: 'gid://gitlab/Packages::Dependency/3', + name: 'Ninject.Extensions.Factory', + versionPattern: '3.3.2', + __typename: 'PackageDependency', + }, + metadata: { + id: 'gid://gitlab/Packages::Nuget::DependencyLinkMetadatum/77', + targetFramework: '.NETCoreApp3.1', + __typename: 'NugetDependencyLinkMetadata', + }, + }, + { + dependencyType: 'DEPENDENCIES', + id: 'gid://gitlab/Packages::DependencyLink/78', + __typename: 'PackageDependencyLink', + dependency: { + id: 'gid://gitlab/Packages::Dependency/4', + name: 'Ninject.Extensions.Factory', + versionPattern: '3.3.2', + __typename: 'PackageDependency', + }, + metadata: { + id: 'gid://gitlab/Packages::Nuget::DependencyLinkMetadatum/78', + targetFramework: '.NETCoreApp3.1', + __typename: 'NugetDependencyLinkMetadata', + }, + }, +]; + +export const packageVersions = () => [ + { + createdAt: '2021-08-10T09:33:54Z', + id: 'gid://gitlab/Packages::Package/243', + name: '@gitlab-org/package-15', + status: 'DEFAULT', + tags: { nodes: packageTags() }, + version: '1.0.1', + __typename: 'Package', + }, + { + createdAt: '2021-08-10T09:33:54Z', + id: 'gid://gitlab/Packages::Package/244', + name: '@gitlab-org/package-15', + status: 'DEFAULT', + tags: { nodes: packageTags() }, + version: '1.0.2', + __typename: 'Package', + }, +]; + +export const packageData = (extend) => ({ + id: 'gid://gitlab/Packages::Package/111', + name: '@gitlab-org/package-15', + packageType: 'NPM', + version: '1.0.0', + createdAt: '2020-08-17T14:23:32Z', + updatedAt: '2020-08-17T14:23:32Z', + status: 'DEFAULT', + ...extend, +}); + +export const conanMetadata = () => ({ + packageChannel: 'stable', + packageUsername: 'gitlab-org+gitlab-test', + recipe: 'package-8/1.0.0@gitlab-org+gitlab-test/stable', + recipePath: 'package-8/1.0.0/gitlab-org+gitlab-test/stable', +}); + +export const composerMetadata = () => ({ + targetSha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0', + composerJson: { + license: 'MIT', + version: '1.0.0', + }, +}); + +export const pypyMetadata = () => ({ + requiredPython: '1.0.0', +}); + +export const mavenMetadata = () => ({ + appName: 'appName', + appGroup: 'appGroup', + appVersion: 'appVersion', + path: 'path', +}); + +export const nugetMetadata = () => ({ + iconUrl: 'iconUrl', + licenseUrl: 'licenseUrl', + projectUrl: 'projectUrl', +}); + +export const packageDetailsQuery = (extendPackage) => ({ + data: { + package: { + ...packageData(), + metadata: { + ...conanMetadata(), + ...composerMetadata(), + ...pypyMetadata(), + ...mavenMetadata(), + ...nugetMetadata(), + }, + project: { + path: 'projectPath', + }, + tags: { + nodes: packageTags(), + __typename: 'PackageTagConnection', + }, + pipelines: { + nodes: packagePipelines(), + __typename: 'PipelineConnection', + }, + packageFiles: { + nodes: packageFiles(), + __typename: 'PackageFileConnection', + }, + versions: { + nodes: packageVersions(), + __typename: 'PackageConnection', + }, + dependencyLinks: { + nodes: dependencyLinks(), + }, + __typename: 'PackageDetailsType', + ...extendPackage, + }, + }, +}); + +export const emptyPackageDetailsQuery = () => ({ + data: { + package: { + __typename: 'PackageDetailsType', + }, + }, +}); + +export const packageDestroyMutation = () => ({ + data: { + destroyPackage: { + errors: [], + }, + }, +}); + +export const packageDestroyMutationError = () => ({ + data: { + destroyPackage: null, + }, + errors: [ + { + message: + "The resource that you are attempting to access does not exist or you don't have permission to perform this action", + locations: [ + { + line: 2, + column: 3, + }, + ], + path: ['destroyPackage'], + }, + ], +}); + +export const packageDestroyFileMutation = () => ({ + data: { + destroyPackageFile: { + errors: [], + }, + }, +}); +export const packageDestroyFileMutationError = () => ({ + data: { + destroyPackageFile: null, + }, + errors: [ + { + message: + "The resource that you are attempting to access does not exist or you don't have permission to perform this action", + locations: [ + { + line: 2, + column: 3, + }, + ], + path: ['destroyPackageFile'], + }, + ], +}); diff --git a/spec/frontend/packages_and_registries/package_registry/utils_spec.js b/spec/frontend/packages_and_registries/package_registry/utils_spec.js new file mode 100644 index 00000000000..019f94aaec2 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/utils_spec.js @@ -0,0 +1,23 @@ +import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils'; + +describe('Packages shared utils', () => { + describe('getPackageTypeLabel', () => { + describe.each` + packageType | expectedResult + ${'CONAN'} | ${'Conan'} + ${'MAVEN'} | ${'Maven'} + ${'NPM'} | ${'npm'} + ${'NUGET'} | ${'NuGet'} + ${'PYPI'} | ${'PyPI'} + ${'RUBYGEMS'} | ${'RubyGems'} + ${'COMPOSER'} | ${'Composer'} + ${'DEBIAN'} | ${'Debian'} + ${'HELM'} | ${'Helm'} + ${'FOO'} | ${null} + `(`package type`, ({ packageType, expectedResult }) => { + it(`${packageType} should show as ${expectedResult}`, () => { + expect(getPackageTypeLabel(packageType)).toBe(expectedResult); + }); + }); + }); +}); |