summaryrefslogtreecommitdiff
path: root/spec/frontend/packages/details/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/packages/details/components')
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap46
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap49
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap34
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap38
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap69
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap69
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap49
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap172
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap50
-rw-r--r--spec/frontend/packages/details/components/additional_metadata_spec.js119
-rw-r--r--spec/frontend/packages/details/components/app_spec.js281
-rw-r--r--spec/frontend/packages/details/components/code_instruction_spec.js110
-rw-r--r--spec/frontend/packages/details/components/composer_installation_spec.js95
-rw-r--r--spec/frontend/packages/details/components/conan_installation_spec.js68
-rw-r--r--spec/frontend/packages/details/components/dependency_row_spec.js62
-rw-r--r--spec/frontend/packages/details/components/history_element_spec.js57
-rw-r--r--spec/frontend/packages/details/components/installations_commands_spec.js57
-rw-r--r--spec/frontend/packages/details/components/maven_installation_spec.js91
-rw-r--r--spec/frontend/packages/details/components/npm_installation_spec.js99
-rw-r--r--spec/frontend/packages/details/components/nuget_installation_spec.js75
-rw-r--r--spec/frontend/packages/details/components/package_history_spec.js106
-rw-r--r--spec/frontend/packages/details/components/package_title_spec.js168
-rw-r--r--spec/frontend/packages/details/components/pypi_installation_spec.js60
23 files changed, 2024 insertions, 0 deletions
diff --git a/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap
new file mode 100644
index 00000000000..172b8919673
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Package code instruction multiline to match the snapshot 1`] = `
+<div>
+ <pre
+ class="js-instruction-pre"
+ >
+ this is some
+multiline text
+ </pre>
+</div>
+`;
+
+exports[`Package code instruction single line to match the default snapshot 1`] = `
+<div
+ class="input-group gl-mb-3"
+>
+ <input
+ class="form-control monospace js-instruction-input"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append js-instruction-button"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-md btn-default"
+ data-clipboard-text="npm i @my-package"
+ title="Copy npm install command"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ class="gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+ </button>
+ </span>
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap
new file mode 100644
index 00000000000..852292e084b
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ConanInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Conan Command
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy Conan Command"
+ instruction="foo/command"
+ trackingaction="copy_conan_command"
+ />
+
+ <h3
+ class="gl-font-lg"
+ >
+ Registry setup
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Add Conan Remote
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy Conan Setup Command"
+ instruction="foo/setup"
+ trackingaction="copy_conan_setup_command"
+ />
+
+ <gl-sprintf-stub
+ message="For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap
new file mode 100644
index 00000000000..28b7ca442eb
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap
@@ -0,0 +1,34 @@
+// 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"
+ >
+ Test.Dependency
+ </strong>
+
+ <span
+ data-testid="target-framework"
+ >
+ (.NETStandard2.0)
+ </span>
+ </div>
+
+ <div
+ class="table-section section-50 gl-display-flex justify-content-md-end"
+ data-testid="version-pattern"
+ >
+ <span
+ class="gl-text-body"
+ >
+ 2.3.7
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap
new file mode 100644
index 00000000000..a1751d69c70
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap
@@ -0,0 +1,38 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`History Element renders the correct markup 1`] = `
+<li
+ class="timeline-entry system-note note-wrapper gl-mb-6!"
+>
+ <div
+ class="timeline-entry-inner"
+ >
+ <div
+ class="timeline-icon"
+ >
+ <gl-icon-stub
+ name="pencil"
+ size="16"
+ />
+ </div>
+
+ <div
+ class="timeline-content"
+ >
+ <div
+ class="note-header"
+ >
+ <span>
+ <div
+ data-testid="default-slot"
+ />
+ </span>
+ </div>
+
+ <div
+ class="note-body"
+ />
+ </div>
+ </div>
+</li>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
new file mode 100644
index 00000000000..10e54500797
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MavenInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Maven XML
+
+ </h4>
+
+ <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="foo/xml"
+ multiline="true"
+ trackingaction="copy_maven_xml"
+ />
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Maven Command
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy Maven command"
+ instruction="foo/command"
+ trackingaction="copy_maven_command"
+ />
+
+ <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="foo/setup"
+ multiline="true"
+ trackingaction="copy_maven_setup_xml"
+ />
+
+ <gl-sprintf-stub
+ message="For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap
new file mode 100644
index 00000000000..58a509e6847
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NpmInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+ npm command
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy npm command"
+ instruction="npm i @Test/package"
+ trackingaction="copy_npm_install_command"
+ />
+
+ <h4
+ class="gl-font-base"
+ >
+ yarn command
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy yarn command"
+ instruction="yarn add @Test/package"
+ trackingaction="copy_yarn_install_command"
+ />
+
+ <h3
+ class="gl-font-lg"
+ >
+ Registry setup
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+ npm command
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy npm setup command"
+ instruction="echo @Test:registry=undefined >> .npmrc"
+ trackingaction="copy_npm_setup_command"
+ />
+
+ <h4
+ class="gl-font-base"
+ >
+ yarn command
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy yarn setup command"
+ instruction="echo \\\\\\"@Test:registry\\\\\\" \\\\\\"undefined\\\\\\" >> .yarnrc"
+ trackingaction="copy_yarn_setup_command"
+ />
+
+ <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/details/components/__snapshots__/nuget_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap
new file mode 100644
index 00000000000..67810290c62
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NugetInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ NuGet Command
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy NuGet Command"
+ instruction="foo/command"
+ trackingaction="copy_nuget_install_command"
+ />
+
+ <h3
+ class="gl-font-lg"
+ >
+ Registry setup
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Add NuGet Source
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy NuGet Setup Command"
+ instruction="foo/setup"
+ trackingaction="copy_nuget_setup_command"
+ />
+
+ <gl-sprintf-stub
+ message="For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap
new file mode 100644
index 00000000000..bdcd4a9e077
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap
@@ -0,0 +1,172 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PackageTitle renders with tags 1`] = `
+<div
+ class="gl-flex-direction-column"
+>
+ <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"
+ >
+
+ Test package
+
+ </h1>
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-gray-500"
+ >
+ <gl-icon-stub
+ class="gl-mr-3"
+ name="eye"
+ size="16"
+ />
+
+ <gl-sprintf-stub
+ message="v%{version} published %{timeAgo}"
+ />
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <gl-icon-stub
+ class="gl-text-gray-500 gl-mr-3"
+ name="package"
+ size="16"
+ />
+
+ <span
+ class="gl-font-weight-bold"
+ data-testid="package-type"
+ >
+ maven
+ </span>
+ </div>
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <package-tags-stub
+ tagdisplaylimit="1"
+ tags="[object Object],[object Object],[object Object],[object Object]"
+ />
+ </div>
+
+ <!---->
+
+ <!---->
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <gl-icon-stub
+ class="gl-text-gray-500 gl-mr-3"
+ name="disk"
+ size="16"
+ />
+
+ <span
+ class="gl-font-weight-bold"
+ data-testid="package-size"
+ >
+ 300 bytes
+ </span>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`PackageTitle renders without tags 1`] = `
+<div
+ class="gl-flex-direction-column"
+>
+ <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"
+ >
+
+ Test package
+
+ </h1>
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-gray-500"
+ >
+ <gl-icon-stub
+ class="gl-mr-3"
+ name="eye"
+ size="16"
+ />
+
+ <gl-sprintf-stub
+ message="v%{version} published %{timeAgo}"
+ />
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <gl-icon-stub
+ class="gl-text-gray-500 gl-mr-3"
+ name="package"
+ size="16"
+ />
+
+ <span
+ class="gl-font-weight-bold"
+ data-testid="package-type"
+ >
+ maven
+ </span>
+ </div>
+
+ <!---->
+
+ <!---->
+
+ <!---->
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <gl-icon-stub
+ class="gl-text-gray-500 gl-mr-3"
+ name="disk"
+ size="16"
+ />
+
+ <span
+ class="gl-font-weight-bold"
+ data-testid="package-size"
+ >
+ 300 bytes
+ </span>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap
new file mode 100644
index 00000000000..5c1e74d73af
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PypiInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Pip Command
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy Pip command"
+ data-testid="pip-command"
+ instruction="pip install"
+ trackingaction="copy_pip_install_command"
+ />
+
+ <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="python setup"
+ multiline="true"
+ trackingaction="copy_pypi_setup_command"
+ />
+
+ <gl-sprintf-stub
+ message="For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/additional_metadata_spec.js b/spec/frontend/packages/details/components/additional_metadata_spec.js
new file mode 100644
index 00000000000..b2337b86740
--- /dev/null
+++ b/spec/frontend/packages/details/components/additional_metadata_spec.js
@@ -0,0 +1,119 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import DetailsRow from '~/registry/shared/components/details_row.vue';
+import component from '~/packages/details/components/additional_metadata.vue';
+
+import { mavenPackage, conanPackage, nugetPackage, npmPackage } from '../../mock_data';
+
+describe('Package Additional Metadata', () => {
+ let wrapper;
+ const defaultProps = {
+ packageEntity: { ...mavenPackage },
+ };
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ propsData: { ...defaultProps, ...props },
+ stubs: {
+ DetailsRow,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findMainArea = () => wrapper.find('[data-testid="main"]');
+ const findNugetSource = () => wrapper.find('[data-testid="nuget-source"]');
+ const findNugetLicense = () => wrapper.find('[data-testid="nuget-license"]');
+ const findConanRecipe = () => wrapper.find('[data-testid="conan-recipe"]');
+ const findMavenApp = () => wrapper.find('[data-testid="maven-app"]');
+ const findMavenGroup = () => wrapper.find('[data-testid="maven-group"]');
+ const findElementLink = container => container.find(GlLink);
+
+ it('has the correct title', () => {
+ mountComponent();
+
+ const title = findTitle();
+
+ expect(title.exists()).toBe(true);
+ expect(title.text()).toBe('Additional Metadata');
+ });
+
+ describe.each`
+ packageEntity | visible | metadata
+ ${mavenPackage} | ${true} | ${'maven_metadatum'}
+ ${conanPackage} | ${true} | ${'conan_metadatum'}
+ ${nugetPackage} | ${true} | ${'nuget_metadatum'}
+ ${npmPackage} | ${false} | ${null}
+ `('Component visibility', ({ packageEntity, visible, metadata }) => {
+ it(`Is ${visible} that the component markup is visible when the package is ${packageEntity.package_type}`, () => {
+ mountComponent({ packageEntity });
+
+ expect(findTitle().exists()).toBe(visible);
+ expect(findMainArea().exists()).toBe(visible);
+ });
+
+ it(`The component is hidden if ${metadata} is missing`, () => {
+ mountComponent({ packageEntity: { ...packageEntity, [metadata]: null } });
+
+ expect(findTitle().exists()).toBe(false);
+ expect(findMainArea().exists()).toBe(false);
+ });
+ });
+
+ describe('nuget metadata', () => {
+ beforeEach(() => {
+ mountComponent({ packageEntity: nugetPackage });
+ });
+
+ it.each`
+ name | finderFunction | text | link | icon
+ ${'source'} | ${findNugetSource} | ${'Source project located at project-foo-url'} | ${'project_url'} | ${'project'}
+ ${'license'} | ${findNugetLicense} | ${'License information located at license-foo-url'} | ${'license_url'} | ${'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.nuget_metadatum[link]);
+ });
+ });
+
+ describe('conan metadata', () => {
+ beforeEach(() => {
+ mountComponent({ packageEntity: conanPackage });
+ });
+
+ it.each`
+ name | finderFunction | text | icon
+ ${'recipe'} | ${findConanRecipe} | ${'Recipe: conan-package/1.0.0@conan+conan-package/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: test-app'} | ${'information-o'}
+ ${'group'} | ${findMavenGroup} | ${'App group: com.test.app'} | ${'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/details/components/app_spec.js b/spec/frontend/packages/details/components/app_spec.js
new file mode 100644
index 00000000000..f535f3f5744
--- /dev/null
+++ b/spec/frontend/packages/details/components/app_spec.js
@@ -0,0 +1,281 @@
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { GlEmptyState, GlModal } from '@gitlab/ui';
+import stubChildren from 'helpers/stub_children';
+import Tracking from '~/tracking';
+import * as getters from '~/packages/details/store/getters';
+import PackagesApp from '~/packages/details/components/app.vue';
+import PackageTitle from '~/packages/details/components/package_title.vue';
+
+import * as SharedUtils from '~/packages/shared/utils';
+import { TrackingActions } from '~/packages/shared/constants';
+import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+import PackageListRow from '~/packages/shared/components/package_list_row.vue';
+
+import DependencyRow from '~/packages/details/components/dependency_row.vue';
+import PackageHistory from '~/packages/details/components/package_history.vue';
+import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
+import InstallationCommands from '~/packages/details/components/installation_commands.vue';
+
+import {
+ composerPackage,
+ conanPackage,
+ mavenPackage,
+ mavenFiles,
+ npmPackage,
+ npmFiles,
+ nugetPackage,
+} from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('PackagesApp', () => {
+ let wrapper;
+ let store;
+ const fetchPackageVersions = jest.fn();
+
+ function createComponent({
+ packageEntity = mavenPackage,
+ packageFiles = mavenFiles,
+ isLoading = false,
+ oneColumnView = false,
+ } = {}) {
+ store = new Vuex.Store({
+ state: {
+ isLoading,
+ packageEntity,
+ packageFiles,
+ canDelete: true,
+ destroyPath: 'destroy-package-path',
+ emptySvgPath: 'empty-illustration',
+ npmPath: 'foo',
+ npmHelpPath: 'foo',
+ projectName: 'bar',
+ oneColumnView,
+ },
+ actions: {
+ fetchPackageVersions,
+ },
+ getters,
+ });
+
+ wrapper = mount(PackagesApp, {
+ localVue,
+ store,
+ stubs: {
+ ...stubChildren(PackagesApp),
+ GlButton: false,
+ GlModal: false,
+ GlTab: false,
+ GlTabs: false,
+ GlTable: false,
+ },
+ });
+ }
+
+ const packageTitle = () => wrapper.find(PackageTitle);
+ const emptyState = () => wrapper.find(GlEmptyState);
+ const allFileRows = () => wrapper.findAll('.js-file-row');
+ const firstFileDownloadLink = () => wrapper.find('.js-file-download');
+ const deleteButton = () => wrapper.find('.js-delete-button');
+ const deleteModal = () => wrapper.find(GlModal);
+ const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' });
+ const versionsTab = () => wrapper.find('.js-versions-tab > a');
+ const packagesLoader = () => wrapper.find(PackagesListLoader);
+ const packagesVersionRows = () => wrapper.findAll(PackageListRow);
+ const noVersionsMessage = () => wrapper.find('[data-testid="no-versions-message"]');
+ const dependenciesTab = () => wrapper.find('.js-dependencies-tab > a');
+ const dependenciesCountBadge = () => wrapper.find('[data-testid="dependencies-badge"]');
+ const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]');
+ const dependencyRows = () => wrapper.findAll(DependencyRow);
+ const findPackageHistory = () => wrapper.find(PackageHistory);
+ const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata);
+ const findInstallationCommands = () => wrapper.find(InstallationCommands);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the app and displays the package title', () => {
+ createComponent();
+
+ expect(packageTitle()).toExist();
+ });
+
+ it('renders an empty state component when no an invalid package is passed as a prop', () => {
+ createComponent({
+ packageEntity: {},
+ });
+
+ expect(emptyState()).toExist();
+ });
+
+ it('package history has the right props', () => {
+ createComponent();
+ expect(findPackageHistory().exists()).toBe(true);
+ expect(findPackageHistory().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
+ expect(findPackageHistory().props('projectName')).toEqual(wrapper.vm.projectName);
+ });
+
+ it('additional metadata has the right props', () => {
+ createComponent();
+ expect(findAdditionalMetadata().exists()).toBe(true);
+ expect(findAdditionalMetadata().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
+ });
+
+ it('installation commands has the right props', () => {
+ createComponent();
+ expect(findInstallationCommands().exists()).toBe(true);
+ expect(findInstallationCommands().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
+ });
+
+ it('hides the files table if package type is COMPOSER', () => {
+ createComponent({ packageEntity: composerPackage });
+ expect(allFileRows().exists()).toBe(false);
+ });
+
+ it('renders a single file for an npm package as they only contain one file', () => {
+ createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
+
+ expect(allFileRows()).toExist();
+ expect(allFileRows()).toHaveLength(1);
+ });
+
+ it('renders multiple files for a package that contains more than one file', () => {
+ createComponent();
+
+ expect(allFileRows()).toExist();
+ expect(allFileRows()).toHaveLength(2);
+ });
+
+ it('allows the user to download a package file by rendering a download link', () => {
+ createComponent();
+
+ expect(allFileRows()).toExist();
+ expect(firstFileDownloadLink().vm.$attrs.href).toContain('download');
+ });
+
+ describe('deleting packages', () => {
+ beforeEach(() => {
+ createComponent();
+ deleteButton().trigger('click');
+ });
+
+ it('shows the delete confirmation modal when delete is clicked', () => {
+ expect(deleteModal()).toExist();
+ });
+ });
+
+ describe('versions', () => {
+ describe('api call', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('makes api request on first click of tab', () => {
+ versionsTab().trigger('click');
+
+ expect(fetchPackageVersions).toHaveBeenCalled();
+ });
+ });
+
+ it('displays the loader when state is loading', () => {
+ createComponent({ isLoading: true });
+
+ expect(packagesLoader().exists()).toBe(true);
+ });
+
+ it('displays the correct version count when the package has versions', () => {
+ createComponent({ packageEntity: npmPackage });
+
+ expect(packagesVersionRows()).toHaveLength(npmPackage.versions.length);
+ });
+
+ it('displays the no versions message when there are none', () => {
+ createComponent();
+
+ expect(noVersionsMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('dependency links', () => {
+ it('does not show the dependency links for a non nuget package', () => {
+ createComponent();
+
+ expect(dependenciesTab().exists()).toBe(false);
+ });
+
+ it('shows the dependencies tab with 0 count when a nuget package with no dependencies', () => {
+ createComponent({
+ packageEntity: {
+ ...nugetPackage,
+ dependency_links: [],
+ },
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ const dependenciesBadge = dependenciesCountBadge();
+
+ expect(dependenciesTab().exists()).toBe(true);
+ expect(dependenciesBadge.exists()).toBe(true);
+ expect(dependenciesBadge.text()).toBe('0');
+ expect(noDependenciesMessage().exists()).toBe(true);
+ });
+ });
+
+ it('renders the correct number of dependency rows for a nuget package', () => {
+ createComponent({ packageEntity: nugetPackage });
+
+ return wrapper.vm.$nextTick(() => {
+ const dependenciesBadge = dependenciesCountBadge();
+
+ expect(dependenciesTab().exists()).toBe(true);
+ expect(dependenciesBadge.exists()).toBe(true);
+ expect(dependenciesBadge.text()).toBe(nugetPackage.dependency_links.length.toString());
+ expect(dependencyRows()).toHaveLength(nugetPackage.dependency_links.length);
+ });
+ });
+ });
+
+ describe('tracking', () => {
+ let eventSpy;
+ let utilSpy;
+ const category = 'foo';
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
+ });
+
+ it('tracking category calls packageTypeToTrackCategory', () => {
+ createComponent({ packageEntity: conanPackage });
+ expect(wrapper.vm.tracking.category).toBe(category);
+ expect(utilSpy).toHaveBeenCalledWith('conan');
+ });
+
+ it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => {
+ createComponent({ packageEntity: conanPackage });
+ deleteButton().trigger('click');
+ return wrapper.vm.$nextTick().then(() => {
+ modalDeleteButton().trigger('click');
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.DELETE_PACKAGE,
+ expect.any(Object),
+ );
+ });
+ });
+
+ it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => {
+ createComponent({ packageEntity: conanPackage });
+
+ firstFileDownloadLink().vm.$emit('click');
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.PULL_PACKAGE,
+ expect.any(Object),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/code_instruction_spec.js b/spec/frontend/packages/details/components/code_instruction_spec.js
new file mode 100644
index 00000000000..724eddb9070
--- /dev/null
+++ b/spec/frontend/packages/details/components/code_instruction_spec.js
@@ -0,0 +1,110 @@
+import { mount } from '@vue/test-utils';
+import CodeInstruction from '~/packages/details/components/code_instruction.vue';
+import { TrackingLabels } from '~/packages/details/constants';
+import Tracking from '~/tracking';
+
+describe('Package code instruction', () => {
+ let wrapper;
+
+ const defaultProps = {
+ instruction: 'npm i @my-package',
+ copyText: 'Copy npm install command',
+ };
+
+ function createComponent(props = {}) {
+ wrapper = mount(CodeInstruction, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ }
+
+ const findInstructionInput = () => wrapper.find('.js-instruction-input');
+ const findInstructionPre = () => wrapper.find('.js-instruction-pre');
+ const findInstructionButton = () => wrapper.find('.js-instruction-button');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('single line', () => {
+ beforeEach(() => createComponent());
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('multiline', () => {
+ beforeEach(() =>
+ createComponent({
+ instruction: 'this is some\nmultiline text',
+ copyText: 'Copy the command',
+ multiline: true,
+ }),
+ );
+
+ it('to match the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('tracking', () => {
+ let eventSpy;
+ const trackingAction = 'test_action';
+ const label = TrackingLabels.CODE_INSTRUCTION;
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ });
+
+ it('should not track when no trackingAction is provided', () => {
+ createComponent();
+ findInstructionButton().trigger('click');
+
+ expect(eventSpy).toHaveBeenCalledTimes(0);
+ });
+
+ describe('when trackingAction is provided for single line', () => {
+ beforeEach(() =>
+ createComponent({
+ trackingAction,
+ }),
+ );
+
+ it('should track when copying from the input', () => {
+ findInstructionInput().trigger('copy');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
+ label,
+ });
+ });
+
+ it('should track when the copy button is pressed', () => {
+ findInstructionButton().trigger('click');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
+ label,
+ });
+ });
+ });
+
+ describe('when trackingAction is provided for multiline', () => {
+ beforeEach(() =>
+ createComponent({
+ trackingAction,
+ multiline: true,
+ }),
+ );
+
+ it('should track when copying from the multiline pre element', () => {
+ findInstructionPre().trigger('copy');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
+ label,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/composer_installation_spec.js b/spec/frontend/packages/details/components/composer_installation_spec.js
new file mode 100644
index 00000000000..7679d721391
--- /dev/null
+++ b/spec/frontend/packages/details/components/composer_installation_spec.js
@@ -0,0 +1,95 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { registryUrl as composerHelpPath } from 'jest/packages/details/mock_data';
+import { composerPackage as packageEntity } from 'jest/packages/mock_data';
+import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { TrackingActions } from '~/packages/details/constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ComposerInstallation', () => {
+ let wrapper;
+
+ const composerRegistryIncludeStr = 'foo/registry';
+ const composerPackageIncludeStr = 'foo/package';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ composerHelpPath,
+ },
+ getters: {
+ composerRegistryInclude: () => composerRegistryIncludeStr,
+ composerPackageInclude: () => composerPackageIncludeStr,
+ },
+ });
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+ const findRegistryIncludeTitle = () => wrapper.find('[data-testid="registry-include-title"]');
+ const findPackageIncludeTitle = () => wrapper.find('[data-testid="package-include-title"]');
+ const findHelpText = () => wrapper.find('[data-testid="help-text"]');
+ const findHelpLink = () => wrapper.find(GlLink);
+
+ function createComponent() {
+ wrapper = shallowMount(ComposerInstallation, {
+ localVue,
+ store,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('registry include command', () => {
+ it('uses code_instructions', () => {
+ const registryIncludeCommand = findCodeInstructions().at(0);
+ expect(registryIncludeCommand.exists()).toBe(true);
+ expect(registryIncludeCommand.props()).toMatchObject({
+ instruction: composerRegistryIncludeStr,
+ copyText: 'Copy registry include',
+ trackingAction: TrackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
+ });
+ });
+
+ it('has the correct title', () => {
+ expect(findRegistryIncludeTitle().text()).toBe('composer.json registry include');
+ });
+ });
+
+ describe('package include command', () => {
+ it('uses code_instructions', () => {
+ const registryIncludeCommand = findCodeInstructions().at(1);
+ expect(registryIncludeCommand.exists()).toBe(true);
+ expect(registryIncludeCommand.props()).toMatchObject({
+ instruction: composerPackageIncludeStr,
+ copyText: 'Copy require package include',
+ trackingAction: TrackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
+ });
+ });
+
+ it('has the correct title', () => {
+ expect(findPackageIncludeTitle().text()).toBe('composer.json require package include');
+ });
+
+ 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',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/conan_installation_spec.js b/spec/frontend/packages/details/components/conan_installation_spec.js
new file mode 100644
index 00000000000..5b31e38dad5
--- /dev/null
+++ b/spec/frontend/packages/details/components/conan_installation_spec.js
@@ -0,0 +1,68 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import ConanInstallation from '~/packages/details/components/conan_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { conanPackage as packageEntity } from '../../mock_data';
+import { registryUrl as conanPath } from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ConanInstallation', () => {
+ let wrapper;
+
+ const conanInstallationCommandStr = 'foo/command';
+ const conanSetupCommandStr = 'foo/setup';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ conanPath,
+ },
+ getters: {
+ conanInstallationCommand: () => conanInstallationCommandStr,
+ conanSetupCommand: () => conanSetupCommandStr,
+ },
+ });
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+
+ function createComponent() {
+ wrapper = shallowMount(ConanInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct command', () => {
+ expect(
+ findCodeInstructions()
+ .at(0)
+ .props('instruction'),
+ ).toBe(conanInstallationCommandStr);
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct command', () => {
+ expect(
+ findCodeInstructions()
+ .at(1)
+ .props('instruction'),
+ ).toBe(conanSetupCommandStr);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/dependency_row_spec.js b/spec/frontend/packages/details/components/dependency_row_spec.js
new file mode 100644
index 00000000000..7d3ee92908d
--- /dev/null
+++ b/spec/frontend/packages/details/components/dependency_row_spec.js
@@ -0,0 +1,62 @@
+import { shallowMount } from '@vue/test-utils';
+import DependencyRow from '~/packages/details/components/dependency_row.vue';
+import { dependencyLinks } from '../../mock_data';
+
+describe('DependencyRow', () => {
+ let wrapper;
+
+ const { withoutFramework, withoutVersion, fullLink } = dependencyLinks;
+
+ function createComponent({ dependencyLink = fullLink } = {}) {
+ wrapper = shallowMount(DependencyRow, {
+ propsData: {
+ dependency: dependencyLink,
+ },
+ });
+ }
+
+ const dependencyVersion = () => wrapper.find('[data-testid="version-pattern"]');
+ const dependencyFramework = () => wrapper.find('[data-testid="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({ dependencyLink: withoutVersion });
+
+ expect(dependencyVersion().exists()).toBe(false);
+ });
+
+ it('does render version info when it exists', () => {
+ createComponent();
+
+ expect(dependencyVersion().exists()).toBe(true);
+ expect(dependencyVersion().text()).toBe(fullLink.version_pattern);
+ });
+ });
+
+ describe('target framework', () => {
+ it('does not render any framework information when not supplied', () => {
+ createComponent({ dependencyLink: withoutFramework });
+
+ expect(dependencyFramework().exists()).toBe(false);
+ });
+
+ it('does render framework info when it exists', () => {
+ createComponent();
+
+ expect(dependencyFramework().exists()).toBe(true);
+ expect(dependencyFramework().text()).toBe(`(${fullLink.target_framework})`);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/history_element_spec.js b/spec/frontend/packages/details/components/history_element_spec.js
new file mode 100644
index 00000000000..e8746fc93f5
--- /dev/null
+++ b/spec/frontend/packages/details/components/history_element_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import component from '~/packages/details/components/history_element.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+describe('History Element', () => {
+ let wrapper;
+ const defaultProps = {
+ icon: 'pencil',
+ };
+
+ const mountComponent = () => {
+ wrapper = shallowMount(component, {
+ propsData: { ...defaultProps },
+ stubs: {
+ TimelineEntryItem,
+ },
+ slots: {
+ default: '<div data-testid="default-slot"></div>',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTimelineEntry = () => wrapper.find(TimelineEntryItem);
+ const findGlIcon = () => wrapper.find(GlIcon);
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+
+ it('renders the correct markup', () => {
+ mountComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has a default slot', () => {
+ mountComponent();
+
+ expect(findDefaultSlot().exists()).toBe(true);
+ });
+ it('has a timeline entry', () => {
+ mountComponent();
+
+ expect(findTimelineEntry().exists()).toBe(true);
+ });
+ it('has an icon', () => {
+ mountComponent();
+
+ const icon = findGlIcon();
+
+ expect(icon.exists()).toBe(true);
+ expect(icon.attributes('name')).toBe(defaultProps.icon);
+ });
+});
diff --git a/spec/frontend/packages/details/components/installations_commands_spec.js b/spec/frontend/packages/details/components/installations_commands_spec.js
new file mode 100644
index 00000000000..60da34ebcd9
--- /dev/null
+++ b/spec/frontend/packages/details/components/installations_commands_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import InstallationCommands from '~/packages/details/components/installation_commands.vue';
+
+import NpmInstallation from '~/packages/details/components/npm_installation.vue';
+import MavenInstallation from '~/packages/details/components/maven_installation.vue';
+import ConanInstallation from '~/packages/details/components/conan_installation.vue';
+import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
+import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
+import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
+
+import {
+ conanPackage,
+ mavenPackage,
+ npmPackage,
+ nugetPackage,
+ pypiPackage,
+ composerPackage,
+} from '../../mock_data';
+
+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.package_type} instructions exist`, () => {
+ createComponent({ packageEntity });
+
+ expect(selector()).toExist();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/maven_installation_spec.js b/spec/frontend/packages/details/components/maven_installation_spec.js
new file mode 100644
index 00000000000..5d0007294b6
--- /dev/null
+++ b/spec/frontend/packages/details/components/maven_installation_spec.js
@@ -0,0 +1,91 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { registryUrl as mavenPath } from 'jest/packages/details/mock_data';
+import { mavenPackage as packageEntity } from 'jest/packages/mock_data';
+import MavenInstallation from '~/packages/details/components/maven_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { TrackingActions } from '~/packages/details/constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('MavenInstallation', () => {
+ let wrapper;
+
+ const xmlCodeBlock = 'foo/xml';
+ const mavenCommandStr = 'foo/command';
+ const mavenSetupXml = 'foo/setup';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ mavenPath,
+ },
+ getters: {
+ mavenInstallationXml: () => xmlCodeBlock,
+ mavenInstallationCommand: () => mavenCommandStr,
+ mavenSetupXml: () => mavenSetupXml,
+ },
+ });
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+
+ function createComponent() {
+ wrapper = shallowMount(MavenInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ 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: TrackingActions.COPY_MAVEN_XML,
+ });
+ });
+
+ it('renders the correct maven command', () => {
+ expect(
+ findCodeInstructions()
+ .at(1)
+ .props(),
+ ).toMatchObject({
+ instruction: mavenCommandStr,
+ multiline: false,
+ trackingAction: TrackingActions.COPY_MAVEN_COMMAND,
+ });
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct xml block', () => {
+ expect(
+ findCodeInstructions()
+ .at(2)
+ .props(),
+ ).toMatchObject({
+ instruction: mavenSetupXml,
+ multiline: true,
+ trackingAction: TrackingActions.COPY_MAVEN_SETUP,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/npm_installation_spec.js b/spec/frontend/packages/details/components/npm_installation_spec.js
new file mode 100644
index 00000000000..f47bac57a66
--- /dev/null
+++ b/spec/frontend/packages/details/components/npm_installation_spec.js
@@ -0,0 +1,99 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { npmPackage as packageEntity } from 'jest/packages/mock_data';
+import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
+import NpmInstallation from '~/packages/details/components/npm_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { TrackingActions } from '~/packages/details/constants';
+import { npmInstallationCommand, npmSetupCommand } from '~/packages/details/store/getters';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('NpmInstallation', () => {
+ let wrapper;
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+
+ function createComponent() {
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ nugetPath,
+ },
+ getters: {
+ npmInstallationCommand,
+ npmSetupCommand,
+ },
+ });
+
+ wrapper = shallowMount(NpmInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct npm command', () => {
+ expect(
+ findCodeInstructions()
+ .at(0)
+ .props(),
+ ).toMatchObject({
+ instruction: 'npm i @Test/package',
+ multiline: false,
+ trackingAction: TrackingActions.COPY_NPM_INSTALL_COMMAND,
+ });
+ });
+
+ it('renders the correct yarn command', () => {
+ expect(
+ findCodeInstructions()
+ .at(1)
+ .props(),
+ ).toMatchObject({
+ instruction: 'yarn add @Test/package',
+ multiline: false,
+ trackingAction: TrackingActions.COPY_YARN_INSTALL_COMMAND,
+ });
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct npm command', () => {
+ expect(
+ findCodeInstructions()
+ .at(2)
+ .props(),
+ ).toMatchObject({
+ instruction: 'echo @Test:registry=undefined >> .npmrc',
+ multiline: false,
+ trackingAction: TrackingActions.COPY_NPM_SETUP_COMMAND,
+ });
+ });
+
+ it('renders the correct yarn command', () => {
+ expect(
+ findCodeInstructions()
+ .at(3)
+ .props(),
+ ).toMatchObject({
+ instruction: 'echo \\"@Test:registry\\" \\"undefined\\" >> .yarnrc',
+ multiline: false,
+ trackingAction: TrackingActions.COPY_YARN_SETUP_COMMAND,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/nuget_installation_spec.js b/spec/frontend/packages/details/components/nuget_installation_spec.js
new file mode 100644
index 00000000000..a23bf9a18a1
--- /dev/null
+++ b/spec/frontend/packages/details/components/nuget_installation_spec.js
@@ -0,0 +1,75 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { nugetPackage as packageEntity } from 'jest/packages/mock_data';
+import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
+import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { TrackingActions } from '~/packages/details/constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('NugetInstallation', () => {
+ let wrapper;
+
+ const nugetInstallationCommandStr = 'foo/command';
+ const nugetSetupCommandStr = 'foo/setup';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ nugetPath,
+ },
+ getters: {
+ nugetInstallationCommand: () => nugetInstallationCommandStr,
+ nugetSetupCommand: () => nugetSetupCommandStr,
+ },
+ });
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+
+ function createComponent() {
+ wrapper = shallowMount(NugetInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct command', () => {
+ expect(
+ findCodeInstructions()
+ .at(0)
+ .props(),
+ ).toMatchObject({
+ instruction: nugetInstallationCommandStr,
+ trackingAction: TrackingActions.COPY_NUGET_INSTALL_COMMAND,
+ });
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct command', () => {
+ expect(
+ findCodeInstructions()
+ .at(1)
+ .props(),
+ ).toMatchObject({
+ instruction: nugetSetupCommandStr,
+ trackingAction: TrackingActions.COPY_NUGET_SETUP_COMMAND,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/package_history_spec.js b/spec/frontend/packages/details/components/package_history_spec.js
new file mode 100644
index 00000000000..e293e119585
--- /dev/null
+++ b/spec/frontend/packages/details/components/package_history_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import component from '~/packages/details/components/package_history.vue';
+
+import { mavenPackage, mockPipelineInfo } from '../../mock_data';
+
+describe('Package History', () => {
+ let wrapper;
+ const defaultProps = {
+ projectName: 'baz project',
+ packageEntity: { ...mavenPackage },
+ };
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ propsData: { ...defaultProps, ...props },
+ stubs: {
+ HistoryElement: '<div data-testid="history-element"><slot></slot></div>',
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findHistoryElement = testId => wrapper.find(`[data-testid="${testId}"]`);
+ const findElementLink = container => container.find(GlLink);
+ const findElementTimeAgo = container => container.find(TimeAgoTooltip);
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findTimeline = () => wrapper.find('[data-testid="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 | icon | text | timeAgoTooltip | link
+ ${'created-on'} | ${'clock'} | ${'Test package version 1.0.0 was created'} | ${mavenPackage.created_at} | ${null}
+ ${'updated-at'} | ${'pencil'} | ${'Test package version 1.0.0 was updated'} | ${mavenPackage.updated_at} | ${null}
+ ${'commit'} | ${'commit'} | ${'Commit sha-baz on branch branch-name'} | ${null} | ${mockPipelineInfo.project.commit_url}
+ ${'pipeline'} | ${'pipeline'} | ${'Pipeline #1 triggered by foo'} | ${mockPipelineInfo.created_at} | ${mockPipelineInfo.project.pipeline_url}
+ ${'published'} | ${'package'} | ${'Published to the baz project Package Registry'} | ${mavenPackage.created_at} | ${null}
+ `('history element $name', ({ name, icon, text, timeAgoTooltip, link }) => {
+ let element;
+
+ beforeEach(() => {
+ mountComponent({ packageEntity: { ...mavenPackage, pipeline: mockPipelineInfo } });
+ element = findHistoryElement(name);
+ });
+
+ 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);
+ }
+ });
+ });
+
+ describe('when pipelineInfo is missing', () => {
+ it.each(['commit', 'pipeline'])('%s history element is hidden', name => {
+ mountComponent();
+ expect(findHistoryElement(name).exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/package_title_spec.js b/spec/frontend/packages/details/components/package_title_spec.js
new file mode 100644
index 00000000000..a30dc4b8aba
--- /dev/null
+++ b/spec/frontend/packages/details/components/package_title_spec.js
@@ -0,0 +1,168 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import PackageTitle from '~/packages/details/components/package_title.vue';
+import PackageTags from '~/packages/shared/components/package_tags.vue';
+import {
+ conanPackage,
+ mavenFiles,
+ mavenPackage,
+ mockTags,
+ npmFiles,
+ npmPackage,
+ nugetPackage,
+} from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('PackageTitle', () => {
+ let wrapper;
+ let store;
+
+ function createComponent({
+ packageEntity = mavenPackage,
+ packageFiles = mavenFiles,
+ icon = null,
+ } = {}) {
+ store = new Vuex.Store({
+ state: {
+ packageEntity,
+ packageFiles,
+ },
+ getters: {
+ packageTypeDisplay: ({ packageEntity: { package_type: type } }) => type,
+ packagePipeline: ({ packageEntity: { pipeline = null } }) => pipeline,
+ packageIcon: () => icon,
+ },
+ });
+
+ wrapper = shallowMount(PackageTitle, {
+ localVue,
+ store,
+ });
+ }
+
+ const packageIcon = () => wrapper.find('[data-testid="package-icon"]');
+ const packageType = () => wrapper.find('[data-testid="package-type"]');
+ const packageSize = () => wrapper.find('[data-testid="package-size"]');
+ const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
+ const packageRef = () => wrapper.find('[data-testid="package-ref"]');
+ const packageTags = () => wrapper.find(PackageTags);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('renders', () => {
+ it('without tags', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('with tags', () => {
+ createComponent({ packageEntity: { ...mavenPackage, tags: mockTags } });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('package icon', () => {
+ const fakeSrc = 'a-fake-src';
+
+ it('shows an icon when provided one from vuex', () => {
+ createComponent({ icon: fakeSrc });
+
+ expect(packageIcon().exists()).toBe(true);
+ });
+
+ it('has the correct src attribute', () => {
+ createComponent({ icon: fakeSrc });
+
+ expect(packageIcon().props('src')).toBe(fakeSrc);
+ });
+
+ it('does not show an icon when not provided one', () => {
+ createComponent();
+
+ expect(packageIcon().exists()).toBe(false);
+ });
+ });
+
+ describe.each`
+ packageEntity | expectedResult
+ ${conanPackage} | ${'conan'}
+ ${mavenPackage} | ${'maven'}
+ ${npmPackage} | ${'npm'}
+ ${nugetPackage} | ${'nuget'}
+ `(`package type`, ({ packageEntity, expectedResult }) => {
+ beforeEach(() => createComponent({ packageEntity }));
+
+ it(`${packageEntity.package_type} should render from Vuex getters ${expectedResult}`, () => {
+ expect(packageType().text()).toBe(expectedResult);
+ });
+ });
+
+ describe('calculates the package size', () => {
+ it('correctly calulates when there is only 1 file', () => {
+ createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
+
+ expect(packageSize().text()).toBe('200 bytes');
+ });
+
+ it('correctly calulates when there are multiple files', () => {
+ createComponent();
+
+ expect(packageSize().text()).toBe('300 bytes');
+ });
+ });
+
+ describe('package tags', () => {
+ it('displays the package-tags component when the package has tags', () => {
+ createComponent({
+ packageEntity: {
+ ...npmPackage,
+ tags: mockTags,
+ },
+ });
+
+ expect(packageTags().exists()).toBe(true);
+ });
+
+ it('does not display the package-tags component when there are no tags', () => {
+ createComponent();
+
+ expect(packageTags().exists()).toBe(false);
+ });
+ });
+
+ describe('package ref', () => {
+ it('does not display the ref if missing', () => {
+ createComponent();
+
+ expect(packageRef().exists()).toBe(false);
+ });
+
+ it('correctly shows the package ref if there is one', () => {
+ createComponent({ packageEntity: npmPackage });
+
+ expect(packageRef().contains('gl-icon-stub')).toBe(true);
+ expect(packageRef().text()).toBe(npmPackage.pipeline.ref);
+ });
+ });
+
+ describe('pipeline project', () => {
+ it('does not display the project if missing', () => {
+ createComponent();
+
+ expect(pipelineProject().exists()).toBe(false);
+ });
+
+ it('correctly shows the pipeline project if there is one', () => {
+ createComponent({ packageEntity: npmPackage });
+
+ expect(pipelineProject().text()).toBe(npmPackage.pipeline.project.name);
+ expect(pipelineProject().attributes('href')).toBe(npmPackage.pipeline.project.web_url);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/pypi_installation_spec.js b/spec/frontend/packages/details/components/pypi_installation_spec.js
new file mode 100644
index 00000000000..da30b4ba565
--- /dev/null
+++ b/spec/frontend/packages/details/components/pypi_installation_spec.js
@@ -0,0 +1,60 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { pypiPackage as packageEntity } from 'jest/packages/mock_data';
+import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('PypiInstallation', () => {
+ let wrapper;
+
+ const pipCommandStr = 'pip install';
+ const pypiSetupStr = 'python setup';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ pypiHelpPath: 'foo',
+ },
+ getters: {
+ pypiPipCommand: () => pipCommandStr,
+ pypiSetupCommand: () => pypiSetupStr,
+ },
+ });
+
+ const pipCommand = () => wrapper.find('[data-testid="pip-command"]');
+ const setupInstruction = () => wrapper.find('[data-testid="pypi-setup-content"]');
+
+ function createComponent() {
+ wrapper = shallowMount(PypiInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct pip command', () => {
+ expect(pipCommand().props('instruction')).toBe(pipCommandStr);
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct setup block', () => {
+ expect(setupInstruction().props('instruction')).toBe(pypiSetupStr);
+ });
+ });
+});