Commit 5f380df9 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch 'nfriend-use-release-frontend-fixture' into 'master'

Update release frontend tests to use JSON fixture

See merge request gitlab-org/gitlab!43752
parents 55cb93cc 95e6139e
......@@ -13,6 +13,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
create(:milestone,
project: project,
title: '12.3',
description: 'The 12.3 milestone',
start_date: Time.zone.parse('2018-12-10'),
due_date: Time.zone.parse('2019-01-10'))
end
......@@ -21,6 +22,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
create(:milestone,
project: project,
title: '12.4',
description: 'The 12.4 milestone',
start_date: Time.zone.parse('2019-01-10'),
due_date: Time.zone.parse('2019-02-10'))
end
......@@ -65,10 +67,26 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
create(:release_link,
release: release,
name: 'Runbook',
url: 'https://example.com/runbook',
url: "#{release.project.web_url}/runbook",
link_type: :runbook)
end
let_it_be(:package_link) do
create(:release_link,
release: release,
name: 'Package',
url: 'https://example.com/package',
link_type: :package)
end
let_it_be(:image_link) do
create(:release_link,
release: release,
name: 'Image',
url: 'https://example.com/image',
link_type: :image)
end
after(:all) do
remove_repository(project)
end
......
......@@ -3,12 +3,15 @@ import { mount } from '@vue/test-utils';
import { merge } from 'lodash';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { getJSONFixture } from 'helpers/fixtures';
import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import { release as originalRelease, milestones as originalMilestones } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalMilestones = originalRelease.milestones;
describe('Release edit/new component', () => {
let wrapper;
let release;
......
......@@ -2,16 +2,12 @@ import { range as rge } from 'lodash';
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { getJSONFixture } from 'helpers/fixtures';
import ReleasesApp from '~/releases/components/app_index.vue';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
import api from '~/api';
import {
pageInfoHeadersWithoutPagination,
pageInfoHeadersWithPagination,
release2 as release,
releases,
} from '../mock_data';
import { pageInfoHeadersWithoutPagination, pageInfoHeadersWithPagination } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
......@@ -25,6 +21,9 @@ jest.mock('~/lib/utils/common_utils', () => ({
const localVue = createLocalVue();
localVue.use(Vuex);
const release = getJSONFixture('api/releases/release.json');
const releases = [release];
describe('Releases App ', () => {
let wrapper;
let fetchReleaseSpy;
......
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import { release as originalRelease } from '../mock_data';
import ReleaseBlock from '~/releases/components/release_block.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release show component', () => {
let wrapper;
let release;
......
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { ENTER_KEY } from '~/lib/utils/keys';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
......@@ -9,6 +9,8 @@ import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release edit component', () => {
let wrapper;
let release;
......@@ -223,12 +225,20 @@ describe('Release edit component', () => {
});
});
it('selects the default asset type if no type was provided by the backend', () => {
describe('when no link type was provided by the backend', () => {
beforeEach(() => {
delete release.assets.links[0].linkType;
factory({ release });
});
it('selects the default asset type', () => {
const selected = wrapper.find({ ref: 'typeSelect' }).element.value;
expect(selected).toBe(DEFAULT_ASSET_LINK_TYPE);
});
});
});
describe('validation', () => {
let linkId;
......
import { mount } from '@vue/test-utils';
import { GlLink, GlIcon } from '@gitlab/ui';
import { getJSONFixture } from 'helpers/fixtures';
import { truncateSha } from '~/lib/utils/text_utility';
import { release as originalRelease } from '../mock_data';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Evidence Block', () => {
let wrapper;
let release;
......@@ -35,7 +37,7 @@ describe('Evidence Block', () => {
});
it('renders the title for the dowload link', () => {
expect(wrapper.find(GlLink).text()).toBe('v1.1.2-evidences-1.json');
expect(wrapper.find(GlLink).text()).toBe(`v1.1-evidences-1.json`);
});
it('renders the correct hover text for the download', () => {
......@@ -43,7 +45,7 @@ describe('Evidence Block', () => {
});
it('renders the correct file link for download', () => {
expect(wrapper.find(GlLink).attributes().download).toBe('v1.1.2-evidences-1.json');
expect(wrapper.find(GlLink).attributes().download).toBe(`v1.1-evidences-1.json`);
});
describe('sha text', () => {
......
import { mount } from '@vue/test-utils';
import { GlCollapse } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { cloneDeep } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockAssets from '~/releases/components/release_block_assets.vue';
import { ASSET_LINK_TYPE } from '~/releases/constants';
import { assets } from '../mock_data';
const { assets } = getJSONFixture('api/releases/release.json');
describe('Release block assets', () => {
let wrapper;
......@@ -31,7 +33,7 @@ describe('Release block assets', () => {
wrapper.findAll('h5').filter(h5 => h5.text() === sections[type]);
beforeEach(() => {
defaultProps = { assets: cloneDeep(assets) };
defaultProps = { assets: convertObjectPropsToCamelCase(assets, { deep: true }) };
});
describe('with default props', () => {
......@@ -43,7 +45,7 @@ describe('Release block assets', () => {
const accordionButton = findAccordionButton();
expect(accordionButton.exists()).toBe(true);
expect(trimText(accordionButton.text())).toBe('Assets 5');
expect(trimText(accordionButton.text())).toBe('Assets 8');
});
it('renders the accordion as expanded by default', () => {
......
import { mount } from '@vue/test-utils';
import { GlLink, GlIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { getJSONFixture } from 'helpers/fixtures';
import { cloneDeep } from 'lodash';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const originalRelease = getJSONFixture('api/releases/release.json');
const mockFutureDate = new Date(9999, 0, 0).toISOString();
let mockIsFutureRelease = false;
......
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import { GlLink } from '@gitlab/ui';
import { getJSONFixture } from 'helpers/fixtures';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { release as originalRelease } from '../mock_data';
import { BACK_URL_PARAM } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release block header', () => {
let wrapper;
let release;
......@@ -49,7 +51,7 @@ describe('Release block header', () => {
});
it('renders the title as text', () => {
expect(findHeader().text()).toBe(release.name);
expect(findHeader().text()).toContain(release.name);
expect(findHeaderLink().exists()).toBe(false);
});
});
......
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import { getJSONFixture } from 'helpers/fixtures';
import { cloneDeep } from 'lodash';
import ReleaseBlockMetadata from '~/releases/components/release_block_metadata.vue';
import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const originalRelease = getJSONFixture('api/releases/release.json');
const mockFutureDate = new Date(9999, 0, 0).toISOString();
let mockIsFutureRelease = false;
......
import { mount } from '@vue/test-utils';
import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { getJSONFixture } from 'helpers/fixtures';
import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue';
import { milestones as originalMilestones } from '../mock_data';
import { MAX_MILESTONES_TO_DISPLAY } from '~/releases/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const { milestones: originalMilestones } = getJSONFixture('api/releases/release.json');
describe('Release block milestone info', () => {
let wrapper;
let milestones;
......@@ -35,7 +37,7 @@ describe('Release block milestone info', () => {
beforeEach(() => factory({ milestones }));
it('renders the correct percentage', () => {
expect(milestoneProgressBarContainer().text()).toContain('41% complete');
expect(milestoneProgressBarContainer().text()).toContain('44% complete');
});
it('renders a progress bar that displays the correct percentage', () => {
......@@ -44,14 +46,24 @@ describe('Release block milestone info', () => {
expect(progressBar.exists()).toBe(true);
expect(progressBar.attributes()).toEqual(
expect.objectContaining({
value: '22',
max: '54',
value: '4',
max: '9',
}),
);
});
it('renders a list of links to all associated milestones', () => {
expect(trimText(milestoneListContainer().text())).toContain('Milestones 13.6 • 13.5');
// The API currently returns the milestones in a non-deterministic order,
// which causes the frontend fixture used by this test to return the
// milestones in one order locally and a different order in the CI pipeline.
// This is a bug and is tracked here: https://gitlab.com/gitlab-org/gitlab/-/issues/259012
// When this bug is fixed this expectation should be updated to
// assert the expected order.
const containerText = trimText(milestoneListContainer().text());
expect(
containerText.includes('Milestones 12.4 • 12.3') ||
containerText.includes('Milestones 12.3 • 12.4'),
).toBe(true);
milestones.forEach((m, i) => {
const milestoneLink = milestoneListContainer()
......@@ -65,7 +77,7 @@ describe('Release block milestone info', () => {
});
it('renders the "Issues" section with a total count of issues associated to the milestone(s)', () => {
const totalIssueCount = 54;
const totalIssueCount = 9;
const issuesContainerText = trimText(issuesContainer().text());
expect(issuesContainerText).toContain(`Issues ${totalIssueCount}`);
......@@ -73,7 +85,7 @@ describe('Release block milestone info', () => {
const badge = issuesContainer().find(GlBadge);
expect(badge.text()).toBe(totalIssueCount.toString());
expect(issuesContainerText).toContain('Open: 32 • Closed: 22');
expect(issuesContainerText).toContain('Open: 5 • Closed: 4');
});
});
......
import $ from 'jquery';
import { mount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import { getJSONFixture } from 'helpers/fixtures';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import * as urlUtility from '~/lib/utils/url_utility';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release block', () => {
let wrapper;
let release;
......@@ -46,7 +48,7 @@ describe('Release block', () => {
beforeEach(() => factory(release));
it("renders the block with an id equal to the release's tag name", () => {
expect(wrapper.attributes().id).toBe('v0.3');
expect(wrapper.attributes().id).toBe(release.tagName);
});
it(`renders an edit button that links to the "Edit release" page with a "${BACK_URL_PARAM}" parameter`, () => {
......@@ -107,7 +109,7 @@ describe('Release block', () => {
});
it('does not render external label when link is not external', () => {
expect(wrapper.find('.js-assets-list li:nth-child(2) a').text()).not.toContain(
expect(wrapper.find('.js-assets-list li:nth-child(3) a').text()).not.toContain(
'external source',
);
});
......
import { ASSET_LINK_TYPE } from '~/releases/constants';
export const milestones = [
{
id: 50,
iid: 2,
project_id: 18,
title: '13.6',
description: 'The 13.6 milestone!',
state: 'active',
created_at: '2019-08-27T17:22:38.280Z',
updated_at: '2019-08-27T17:22:38.280Z',
due_date: '2019-09-19',
start_date: '2019-08-31',
web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/2',
issue_stats: {
total: 33,
closed: 19,
},
},
{
id: 49,
iid: 1,
project_id: 18,
title: '13.5',
description: 'The 13.5 milestone!',
state: 'active',
created_at: '2019-08-26T17:55:48.643Z',
updated_at: '2019-08-26T17:55:48.643Z',
due_date: '2019-10-11',
start_date: '2019-08-19',
web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/1',
issue_stats: {
total: 21,
closed: 3,
},
},
];
export const release = {
name: 'New release',
tag_name: 'v0.3',
tag_path: '/root/release-test/-/tags/v0.3',
description: 'A super nice release!',
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
created_at: '2019-08-26T17:54:04.952Z',
released_at: '2019-08-26T17:54:04.807Z',
author: {
id: 1,
name: 'Administrator',
username: 'root',
state: 'active',
avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'http://0.0.0.0:3001/root',
},
commit: {
id: 'c22b0728d1b465f82898c884d32b01aa642f96c1',
short_id: 'c22b0728',
created_at: '2019-08-26T17:47:07.000Z',
parent_ids: [],
title: 'Initial commit',
message: 'Initial commit',
author_name: 'Administrator',
author_email: 'admin@example.com',
authored_date: '2019-08-26T17:47:07.000Z',
committer_name: 'Administrator',
committer_email: 'admin@example.com',
committed_date: '2019-08-26T17:47:07.000Z',
},
commit_path: '/root/release-test/commit/c22b0728d1b465f82898c884d32b01aa642f96c1',
upcoming_release: false,
milestones,
evidences: [
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/1.json',
sha: 'fb3a125fd69a0e5048ebfb0ba43eb32ce4911520dd8d',
collected_at: '2018-10-19 15:43:20 +0200',
},
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/2.json',
sha: '6ebd17a66e6a861175735416e49cf677678029805712dd71bb805c609e2d9108',
collected_at: '2018-10-19 15:43:20 +0200',
},
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/3.json',
sha: '2f65beaf275c3cb4b4e24fb01d481cc475d69c957830833f15338384816b5cba',
collected_at: '2018-10-19 15:43:20 +0200',
},
],
assets: {
count: 5,
sources: [
{
format: 'zip',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.zip',
},
{
format: 'tar.gz',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.gz',
},
{
format: 'tar.bz2',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.bz2',
},
{
format: 'tar',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar',
},
],
links: [
{
id: 1,
name: 'my link',
url: 'https://google.com',
direct_asset_url: 'https://redirected.google.com',
external: true,
},
{
id: 2,
name: 'my second link',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
direct_asset_url: 'https://redirected.google.com',
external: false,
},
],
},
_links: {
self: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3',
edit_url: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit',
},
};
export const pageInfoHeadersWithoutPagination = {
'X-NEXT-PAGE': '',
'X-PAGE': '1',
......@@ -152,77 +16,6 @@ export const pageInfoHeadersWithPagination = {
'X-TOTAL-PAGES': '2',
};
export const assets = {
count: 5,
sources: [
{
format: 'zip',
url: 'https://example.gitlab.com/path/to/zip',
},
],
links: [
{
linkType: ASSET_LINK_TYPE.IMAGE,
url: 'https://example.gitlab.com/path/to/image',
directAssetUrl: 'https://example.gitlab.com/path/to/image',
name: 'Example image link',
},
{
linkType: ASSET_LINK_TYPE.PACKAGE,
url: 'https://example.gitlab.com/path/to/package',
directAssetUrl: 'https://example.gitlab.com/path/to/package',
name: 'Example package link',
},
{
linkType: ASSET_LINK_TYPE.RUNBOOK,
url: 'https://example.gitlab.com/path/to/runbook',
directAssetUrl: 'https://example.gitlab.com/path/to/runbook',
name: 'Example runbook link',
},
{
linkType: ASSET_LINK_TYPE.OTHER,
url: 'https://example.gitlab.com/path/to/link',
directAssetUrl: 'https://example.gitlab.com/path/to/link',
name: 'Example link',
},
],
};
export const release2 = {
name: 'Bionic Beaver',
tag_name: '18.04',
description: '## changelog\n\n* line 1\n* line2',
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot',
author_email: 'release-bot@example.com',
created_at: '2012-05-28T05:00:00-07:00',
commit: {
id: '2695effb5807a22ff3d138d593fd856244e155e7',
short_id: '2695effb',
title: 'Initial commit',
created_at: '2017-07-26T11:08:53.000+02:00',
parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
message: 'Initial commit',
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
name: 'John Doe',
path: '/johndoe',
state: 'active',
status_tooltip_html: null,
username: 'johndoe',
web_url: 'https://gitlab.com/johndoe',
},
authored_date: '2012-05-28T04:42:42-07:00',
committer_name: 'Jack Smith',
committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00',
},
assets,
};
export const releases = [release, release2];
export const graphqlReleasesResponse = {
data: {
project: {
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { getJSONFixture } from 'helpers/fixtures';
import { cloneDeep } from 'lodash';
import * as actions from '~/releases/stores/modules/detail/actions';
import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release as originalRelease } from '../../../mock_data';
import createState from '~/releases/stores/modules/detail/state';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
......@@ -21,6 +21,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
}));
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release detail actions', () => {
let state;
let release;
......
import { getJSONFixture } from 'helpers/fixtures';
import createState from '~/releases/stores/modules/detail/state';
import mutations from '~/releases/stores/modules/detail/mutations';
import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release as originalRelease } from '../../../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release detail mutations', () => {
let state;
let release;
......
import { cloneDeep } from 'lodash';
import testAction from 'helpers/vuex_action_helper';
import { getJSONFixture } from 'helpers/fixtures';
import {
fetchReleases,
fetchReleasesGraphQl,
......@@ -17,12 +18,14 @@ import {
} from '~/lib/utils/common_utils';
import {
pageInfoHeadersWithoutPagination,
releases as originalReleases,
graphqlReleasesResponse as originalGraphqlReleasesResponse,
} from '../../../mock_data';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { PAGE_SIZE } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
describe('Releases State actions', () => {
let mockedState;
let releases;
......
import { getJSONFixture } from 'helpers/fixtures';
import createState from '~/releases/stores/modules/list/state';
import mutations from '~/releases/stores/modules/list/mutations';
import * as types from '~/releases/stores/modules/list/mutation_types';
import { parseIntPagination } from '~/lib/utils/common_utils';
import {
pageInfoHeadersWithoutPagination,
releases,
graphqlReleasesResponse,
} from '../../../mock_data';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination, graphqlReleasesResponse } from '../../../mock_data';
import { convertGraphQLResponse } from '~/releases/util';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
describe('Releases Store Mutations', () => {
let stateCopy;
let restPageInfo;
let graphQlPageInfo;
let releases;
beforeEach(() => {
stateCopy = createState({});
restPageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
graphQlPageInfo = convertGraphQLResponse(graphqlReleasesResponse).paginationInfo;
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
});
describe('REQUEST_RELEASES', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment