summaryrefslogtreecommitdiff
path: root/spec/frontend/packages_and_registries
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
commitb76ae638462ab0f673e5915986070518dd3f9ad3 (patch)
treebdab0533383b52873be0ec0eb4d3c66598ff8b91 /spec/frontend/packages_and_registries
parent434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff)
downloadgitlab-ce-8c890596f5d0792c467fe12805ab1b39f93bf140.tar.gz
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'spec/frontend/packages_and_registries')
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap36
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap36
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap30
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap135
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap36
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap36
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap197
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap48
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap101
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js130
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js448
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js118
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js65
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js69
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js33
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js58
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js64
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js213
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js122
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js75
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js272
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js122
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js202
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js80
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js89
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js251
-rw-r--r--spec/frontend/packages_and_registries/package_registry/utils_spec.js23
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);
+ });
+ });
+ });
+});