2020-01-01 13:55:28 +05:30
|
|
|
import { mount } from '@vue/test-utils';
|
2022-06-21 17:19:12 +05:30
|
|
|
import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
|
2021-03-11 19:13:27 +05:30
|
|
|
import { cloneDeep } from 'lodash';
|
2020-01-01 13:55:28 +05:30
|
|
|
import { format } from 'timeago.js';
|
2021-10-27 15:23:28 +05:30
|
|
|
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
|
|
|
|
import ActionsComponent from '~/environments/components/environment_actions.vue';
|
2021-03-11 19:13:27 +05:30
|
|
|
import DeleteComponent from '~/environments/components/environment_delete.vue';
|
2021-10-27 15:23:28 +05:30
|
|
|
import ExternalUrlComponent from '~/environments/components/environment_external_url.vue';
|
2020-01-01 13:55:28 +05:30
|
|
|
import EnvironmentItem from '~/environments/components/environment_item.vue';
|
2020-03-13 15:44:24 +05:30
|
|
|
import PinComponent from '~/environments/components/environment_pin.vue';
|
2021-10-27 15:23:28 +05:30
|
|
|
import RollbackComponent from '~/environments/components/environment_rollback.vue';
|
|
|
|
import StopComponent from '~/environments/components/environment_stop.vue';
|
|
|
|
import TerminalButtonComponent from '~/environments/components/environment_terminal_button.vue';
|
2020-11-24 15:15:51 +05:30
|
|
|
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
|
2020-01-01 13:55:28 +05:30
|
|
|
import { environment, folder, tableData } from './mock_data';
|
|
|
|
|
|
|
|
describe('Environment item', () => {
|
|
|
|
let wrapper;
|
2021-10-27 15:23:28 +05:30
|
|
|
let tracking;
|
2020-01-01 13:55:28 +05:30
|
|
|
|
|
|
|
const factory = (options = {}) => {
|
|
|
|
wrapper = mount(EnvironmentItem, {
|
|
|
|
...options,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
factory({
|
|
|
|
propsData: {
|
|
|
|
model: environment,
|
|
|
|
tableData,
|
|
|
|
},
|
|
|
|
});
|
2021-10-27 15:23:28 +05:30
|
|
|
|
|
|
|
tracking = mockTracking(undefined, wrapper.element, jest.spyOn);
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
unmockTracking();
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
const findAutoStop = () => wrapper.find('.js-auto-stop');
|
2021-02-22 17:27:13 +05:30
|
|
|
const findUpcomingDeployment = () => wrapper.find('[data-testid="upcoming-deployment"]');
|
2022-06-21 17:19:12 +05:30
|
|
|
const findLastDeployment = () => wrapper.find('[data-testid="environment-deployment-id-cell"]');
|
2021-02-22 17:27:13 +05:30
|
|
|
const findUpcomingDeploymentContent = () =>
|
|
|
|
wrapper.find('[data-testid="upcoming-deployment-content"]');
|
|
|
|
const findUpcomingDeploymentStatusLink = () =>
|
|
|
|
wrapper.find('[data-testid="upcoming-deployment-status-link"]');
|
2022-06-21 17:19:12 +05:30
|
|
|
const findLastDeploymentAvatarLink = () => findLastDeployment().findComponent(GlAvatarLink);
|
|
|
|
const findLastDeploymentAvatar = () => findLastDeployment().findComponent(GlAvatar);
|
|
|
|
const findUpcomingDeploymentAvatarLink = () =>
|
|
|
|
findUpcomingDeployment().findComponent(GlAvatarLink);
|
|
|
|
const findUpcomingDeploymentAvatar = () => findUpcomingDeployment().findComponent(GlAvatar);
|
2023-07-09 08:55:56 +05:30
|
|
|
const findMonitoringLink = () => wrapper.find('[data-testid="environment-monitoring"]');
|
2020-03-13 15:44:24 +05:30
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
describe('when item is not folder', () => {
|
|
|
|
it('should render environment name', () => {
|
|
|
|
expect(wrapper.find('.environment-name').text()).toContain(environment.name);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('With deployment', () => {
|
|
|
|
it('should render deployment internal id', () => {
|
|
|
|
expect(wrapper.find('.deployment-column span').text()).toContain(
|
2022-08-13 15:12:31 +05:30
|
|
|
environment.last_deployment.iid.toString(),
|
2020-01-01 13:55:28 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
expect(wrapper.find('.deployment-column span').text()).toContain('#');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render last deployment date', () => {
|
|
|
|
const formattedDate = format(environment.last_deployment.deployed_at);
|
|
|
|
|
|
|
|
expect(wrapper.find('.environment-created-date-timeago').text()).toContain(formattedDate);
|
|
|
|
});
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
it('should not render the delete button', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
expect(wrapper.findComponent(DeleteComponent).exists()).toBe(false);
|
2020-04-22 19:07:51 +05:30
|
|
|
});
|
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
describe('With user information', () => {
|
|
|
|
it('should render user avatar with link to profile', () => {
|
2022-06-21 17:19:12 +05:30
|
|
|
const avatarLink = findLastDeploymentAvatarLink();
|
|
|
|
const avatar = findLastDeploymentAvatar();
|
2022-08-27 11:52:29 +05:30
|
|
|
const { username, avatar_url: src, web_url } = environment.last_deployment.user;
|
2022-06-21 17:19:12 +05:30
|
|
|
|
|
|
|
expect(avatarLink.attributes('href')).toBe(web_url);
|
|
|
|
expect(avatar.props()).toMatchObject({
|
2022-08-27 11:52:29 +05:30
|
|
|
src,
|
2022-06-21 17:19:12 +05:30
|
|
|
entityName: username,
|
|
|
|
});
|
|
|
|
expect(avatar.attributes()).toMatchObject({
|
|
|
|
title: username,
|
|
|
|
alt: `${username}'s avatar`,
|
|
|
|
});
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('With build url', () => {
|
|
|
|
it('should link to build url provided', () => {
|
|
|
|
expect(wrapper.find('.build-link').attributes('href')).toEqual(
|
|
|
|
environment.last_deployment.deployable.build_path,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render deployable name and id', () => {
|
|
|
|
expect(wrapper.find('.build-link').attributes('href')).toEqual(
|
|
|
|
environment.last_deployment.deployable.build_path,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('With commit information', () => {
|
|
|
|
it('should render commit component', () => {
|
|
|
|
expect(wrapper.find('.js-commit-component')).toBeDefined();
|
|
|
|
});
|
|
|
|
});
|
2020-03-13 15:44:24 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
describe('When the envionment has an upcoming deployment', () => {
|
|
|
|
describe('When the upcoming deployment has a deployable', () => {
|
|
|
|
it('should render the build ID and user', () => {
|
2022-06-21 17:19:12 +05:30
|
|
|
const avatarLink = findUpcomingDeploymentAvatarLink();
|
|
|
|
const avatar = findUpcomingDeploymentAvatar();
|
2022-08-27 11:52:29 +05:30
|
|
|
const { username, avatar_url: src, web_url } = environment.upcoming_deployment.user;
|
2022-06-21 17:19:12 +05:30
|
|
|
|
|
|
|
expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by');
|
|
|
|
expect(avatarLink.attributes('href')).toBe(web_url);
|
|
|
|
expect(avatar.props()).toMatchObject({
|
2022-08-27 11:52:29 +05:30
|
|
|
src,
|
2022-06-21 17:19:12 +05:30
|
|
|
entityName: username,
|
|
|
|
});
|
2021-02-22 17:27:13 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
it('should render a status icon with a link and tooltip', () => {
|
|
|
|
expect(findUpcomingDeploymentStatusLink().exists()).toBe(true);
|
|
|
|
|
|
|
|
expect(findUpcomingDeploymentStatusLink().attributes().href).toBe(
|
|
|
|
'/root/environment-test/-/jobs/892',
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(findUpcomingDeploymentStatusLink().attributes().title).toBe(
|
|
|
|
'Deployment running',
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('When the deployment does not have a deployable', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
const environmentWithoutDeployable = cloneDeep(environment);
|
|
|
|
delete environmentWithoutDeployable.upcoming_deployment.deployable;
|
|
|
|
|
|
|
|
factory({
|
|
|
|
propsData: {
|
|
|
|
model: environmentWithoutDeployable,
|
|
|
|
tableData,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-06-21 17:19:12 +05:30
|
|
|
it('should still render the build ID and user avatar', () => {
|
|
|
|
const avatarLink = findUpcomingDeploymentAvatarLink();
|
|
|
|
const avatar = findUpcomingDeploymentAvatar();
|
2022-08-27 11:52:29 +05:30
|
|
|
const { username, avatar_url: src, web_url } = environment.upcoming_deployment.user;
|
2022-06-21 17:19:12 +05:30
|
|
|
|
|
|
|
expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by');
|
|
|
|
expect(avatarLink.attributes('href')).toBe(web_url);
|
|
|
|
expect(avatar.props()).toMatchObject({
|
2022-08-27 11:52:29 +05:30
|
|
|
src,
|
2022-06-21 17:19:12 +05:30
|
|
|
entityName: username,
|
|
|
|
});
|
2021-02-22 17:27:13 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
it('should not render the status icon', () => {
|
|
|
|
expect(findUpcomingDeploymentStatusLink().exists()).toBe(false);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Without upcoming deployment', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
const environmentWithoutUpcomingDeployment = cloneDeep(environment);
|
|
|
|
delete environmentWithoutUpcomingDeployment.upcoming_deployment;
|
|
|
|
|
|
|
|
factory({
|
|
|
|
propsData: {
|
|
|
|
model: environmentWithoutUpcomingDeployment,
|
|
|
|
tableData,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not render anything in the upcoming deployment column', () => {
|
|
|
|
expect(findUpcomingDeploymentContent().exists()).toBe(false);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
describe('Without auto-stop date', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
factory({
|
|
|
|
propsData: {
|
|
|
|
model: environment,
|
|
|
|
tableData,
|
|
|
|
shouldShowAutoStopDate: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not render a date', () => {
|
|
|
|
expect(findAutoStop().exists()).toBe(false);
|
|
|
|
});
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
it('should not render the auto-stop button', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
expect(wrapper.findComponent(PinComponent).exists()).toBe(false);
|
2020-03-13 15:44:24 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('With auto-stop date', () => {
|
|
|
|
describe('in the future', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
let pin;
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
const futureDate = new Date(Date.now() + 100000);
|
|
|
|
beforeEach(() => {
|
|
|
|
factory({
|
|
|
|
propsData: {
|
|
|
|
model: {
|
|
|
|
...environment,
|
|
|
|
auto_stop_at: futureDate,
|
|
|
|
},
|
|
|
|
tableData,
|
|
|
|
shouldShowAutoStopDate: true,
|
|
|
|
},
|
|
|
|
});
|
2021-10-27 15:23:28 +05:30
|
|
|
tracking = mockTracking(undefined, wrapper.element, jest.spyOn);
|
|
|
|
|
|
|
|
pin = wrapper.findComponent(PinComponent);
|
2020-03-13 15:44:24 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
it('renders the date', () => {
|
|
|
|
expect(findAutoStop().text()).toContain(format(futureDate));
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render the auto-stop button', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
expect(pin.exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should tracks clicks', () => {
|
|
|
|
pin.trigger('click');
|
|
|
|
|
|
|
|
expect(tracking).toHaveBeenCalledWith('_category_', 'click_button', {
|
|
|
|
label: 'environment_pin',
|
|
|
|
});
|
2020-03-13 15:44:24 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('in the past', () => {
|
2020-11-24 15:15:51 +05:30
|
|
|
const pastDate = new Date(differenceInMilliseconds(100000));
|
2020-03-13 15:44:24 +05:30
|
|
|
beforeEach(() => {
|
|
|
|
factory({
|
|
|
|
propsData: {
|
|
|
|
model: {
|
|
|
|
...environment,
|
|
|
|
auto_stop_at: pastDate,
|
|
|
|
},
|
|
|
|
tableData,
|
|
|
|
shouldShowAutoStopDate: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not render a date', () => {
|
|
|
|
expect(findAutoStop().exists()).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not render the suto-stop button', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
expect(wrapper.findComponent(PinComponent).exists()).toBe(false);
|
2020-03-13 15:44:24 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
describe('With manual actions', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
let actions;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
actions = wrapper.findComponent(ActionsComponent);
|
|
|
|
});
|
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
it('should render actions component', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
expect(actions.exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should track clicks', () => {
|
|
|
|
actions.trigger('click');
|
|
|
|
expect(tracking).toHaveBeenCalledWith('_category_', 'click_dropdown', {
|
|
|
|
label: 'environment_actions',
|
|
|
|
});
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('With external URL', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
let externalUrl;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
externalUrl = wrapper.findComponent(ExternalUrlComponent);
|
|
|
|
});
|
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
it('should render external url component', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
expect(externalUrl.exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should track clicks', () => {
|
|
|
|
externalUrl.trigger('click');
|
|
|
|
expect(tracking).toHaveBeenCalledWith('_category_', 'click_button', {
|
|
|
|
label: 'environment_url',
|
|
|
|
});
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('With stop action', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
let stop;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
stop = wrapper.findComponent(StopComponent);
|
|
|
|
});
|
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
it('should render stop action component', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
expect(stop.exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should track clicks', () => {
|
|
|
|
stop.trigger('click');
|
|
|
|
expect(tracking).toHaveBeenCalledWith('_category_', 'click_button', {
|
|
|
|
label: 'environment_stop',
|
|
|
|
});
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('With retry action', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
let rollback;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
rollback = wrapper.findComponent(RollbackComponent);
|
|
|
|
});
|
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
it('should render rollback component', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
expect(rollback.exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should track clicks', () => {
|
|
|
|
rollback.trigger('click');
|
|
|
|
expect(tracking).toHaveBeenCalledWith('_category_', 'click_button', {
|
|
|
|
label: 'environment_rollback',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('With terminal path', () => {
|
|
|
|
let terminal;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
terminal = wrapper.findComponent(TerminalButtonComponent);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render terminal action component', () => {
|
|
|
|
expect(terminal.exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should track clicks', () => {
|
|
|
|
triggerEvent(terminal.element);
|
|
|
|
expect(tracking).toHaveBeenCalledWith('_category_', 'click_button', {
|
|
|
|
label: 'environment_terminal',
|
|
|
|
});
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('When item is folder', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
factory({
|
|
|
|
propsData: {
|
|
|
|
model: folder,
|
|
|
|
tableData,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render folder icon and name', () => {
|
|
|
|
expect(wrapper.find('.folder-name').text()).toContain(folder.name);
|
|
|
|
expect(wrapper.find('.folder-icon')).toBeDefined();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render the number of children in a badge', () => {
|
2022-08-13 15:12:31 +05:30
|
|
|
expect(wrapper.find('.folder-name .badge').text()).toContain(folder.size.toString());
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|
2021-02-22 17:27:13 +05:30
|
|
|
|
|
|
|
it('should not render the "Upcoming deployment" column', () => {
|
|
|
|
expect(findUpcomingDeployment().exists()).toBe(false);
|
|
|
|
});
|
2021-09-30 23:02:18 +05:30
|
|
|
|
|
|
|
it('should set the name cell to be full width', () => {
|
|
|
|
expect(wrapper.find('[data-testid="environment-name-cell"]').classes('section-100')).toBe(
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should hide non-folder properties', () => {
|
2022-06-21 17:19:12 +05:30
|
|
|
expect(findLastDeployment().exists()).toBe(false);
|
2021-09-30 23:02:18 +05:30
|
|
|
expect(wrapper.find('[data-testid="environment-build-cell"]').exists()).toBe(false);
|
|
|
|
});
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|
2020-04-22 19:07:51 +05:30
|
|
|
|
|
|
|
describe('When environment can be deleted', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
factory({
|
|
|
|
propsData: {
|
|
|
|
model: {
|
|
|
|
can_delete: true,
|
|
|
|
delete_path: 'http://0.0.0.0:3000/api/v4/projects/8/environments/45',
|
|
|
|
},
|
|
|
|
tableData,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render the delete button', () => {
|
2021-10-27 15:23:28 +05:30
|
|
|
expect(wrapper.findComponent(DeleteComponent).exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should trigger a tracking event', async () => {
|
|
|
|
tracking = mockTracking(undefined, wrapper.element, jest.spyOn);
|
|
|
|
|
|
|
|
await wrapper.findComponent(DeleteComponent).trigger('click');
|
|
|
|
|
|
|
|
expect(tracking).toHaveBeenCalledWith('_category_', 'click_button', {
|
|
|
|
label: 'environment_delete',
|
|
|
|
});
|
2020-04-22 19:07:51 +05:30
|
|
|
});
|
|
|
|
});
|
2023-07-09 08:55:56 +05:30
|
|
|
|
|
|
|
describe.each([true, false])(
|
|
|
|
'when `remove_monitor_metrics` flag is %p',
|
|
|
|
(removeMonitorMetrics) => {
|
|
|
|
beforeEach(() => {
|
|
|
|
factory({
|
|
|
|
propsData: {
|
|
|
|
model: {
|
|
|
|
metrics_path: 'http://0.0.0.0:3000/flightjs/Flight/-/metrics?environment=6',
|
|
|
|
},
|
|
|
|
tableData,
|
|
|
|
},
|
|
|
|
provide: { glFeatures: { removeMonitorMetrics } },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it(`${removeMonitorMetrics ? 'does not render' : 'renders'} link to metrics`, () => {
|
|
|
|
expect(findMonitoringLink().exists()).toBe(!removeMonitorMetrics);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
2020-01-01 13:55:28 +05:30
|
|
|
});
|