Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-03-29 12:09:41 +00:00
parent f33d86ffac
commit 0dd9f7b364
19 changed files with 156 additions and 133 deletions

View File

@ -44,10 +44,10 @@ export function createProject(projectData) {
});
}
export function deleteProject(projectId) {
export function deleteProject(projectId, params) {
const url = buildApiUrl(PROJECT_PATH).replace(':id', projectId);
return axios.delete(url);
return axios.delete(url, { params });
}
export function importProjectMembers(sourceId, targetId) {

View File

@ -7,6 +7,7 @@
export const organizationProjects = [
{
id: 'gid://gitlab/Project/8',
fullPath: 'project/8',
nameWithNamespace: 'Twitter / Typeahead.Js',
organizationEditPath: '/-/organizations/default/projects/twitter/Typeahead.Js/edit',
webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
@ -40,6 +41,7 @@ export const organizationProjects = [
},
{
id: 'gid://gitlab/Project/7',
fullPath: 'project/7',
nameWithNamespace: 'Flightjs / Flight',
organizationEditPath: '/-/organizations/default/projects/flightjs/Flight/edit',
webUrl: 'http://127.0.0.1:3000/flightjs/Flight',
@ -73,6 +75,7 @@ export const organizationProjects = [
},
{
id: 'gid://gitlab/Project/6',
fullPath: 'project/6',
nameWithNamespace: 'Jashkenas / Underscore',
organizationEditPath: '/-/organizations/default/projects/jashkenas/Underscore/edit',
webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore',
@ -106,6 +109,7 @@ export const organizationProjects = [
},
{
id: 'gid://gitlab/Project/5',
fullPath: 'project/5',
nameWithNamespace: 'Commit451 / Lab Coat',
organizationEditPath: '/-/organizations/default/projects/Commit451/lab-coat/edit',
webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat',
@ -139,6 +143,7 @@ export const organizationProjects = [
},
{
id: 'gid://gitlab/Project/1',
fullPath: 'project/1',
nameWithNamespace: 'Toolbox / Gitlab Smoke Tests',
organizationEditPath: '/-/organizations/default/projects/toolbox/gitlab-smoke-tests/edit',
webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests',

View File

@ -1,5 +1,6 @@
fragment BaseProject on Project {
id
fullPath
archived
nameWithNamespace
organizationEditPath

View File

@ -113,10 +113,7 @@ export default {
<div class="hide-collapsed gl-line-height-20 gl-font-weight-bold">
{{ contactsLabel }}
</div>
<div
class="hide-collapsed gl-display-flex gl-flex-wrap"
:class="contacts.length > 0 ? 'gl-mt-2' : ''"
>
<div v-if="shouldShowContacts" class="hide-collapsed gl-display-flex gl-flex-wrap gl-mt-2">
<div
v-for="(contact, index) in contacts"
:id="`contact_container_${index}`"
@ -137,5 +134,12 @@ export default {
</gl-popover>
</div>
</div>
<div
v-else
data-testid="crm-empty-message"
class="gl-display-flex gl-align-items-center hide-collapsed gl-text-gray-500"
>
{{ __('To add active contacts, use /add_contacts.') }}
</div>
</div>
</template>

View File

@ -4,7 +4,6 @@ import {
GlSprintf,
GlModal,
GlAlert,
GlLoadingIcon,
GlDisclosureDropdown,
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
@ -40,7 +39,6 @@ export default {
GlSprintf,
GlModal,
GlAlert,
GlLoadingIcon,
GlDisclosureDropdown,
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
@ -347,7 +345,6 @@ export default {
</div>
<gl-modal
ref="deleteModal"
v-model="isDeleteModalVisible"
modal-id="delete-modal"
:title="__('Delete snippet modal')"
@ -374,11 +371,10 @@ export default {
<gl-button
variant="danger"
category="primary"
:disabled="isLoading"
:loading="isLoading"
data-testid="delete-snippet-button"
@click="deleteSnippet"
>
<gl-loading-icon v-if="isLoading" size="sm" inline />
{{ __('Delete snippet') }}
</gl-button>
</template>

View File

@ -73,7 +73,7 @@ export default {
</template>
</template>
<span v-else class="gl-text-secondary">
{{ s__('TimeTracking|Use /spend or /estimate to manage time.') }}
{{ s__('TimeTracking|To manage time, use /spend or /estimate.') }}
</span>
</div>
</div>

View File

@ -1,67 +0,0 @@
# Use this template to announce a feature deprecation or other
# important planned changes at least three releases prior to removal.
# Breaking changes must happen in a major release.
#
# See the deprecation guidelines to confirm your understanding of GitLab's definitions:
# https://docs.gitlab.com/ee/development/deprecation_guidelines/#terminology
#
# If an End of Support period applies, see the OPTIONAL section below.
#
# For more information, see the handbook:
# https://handbook.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-and-other-planned-breaking-change-announcements
# ===================
# REQUIRED FIELDS
# ===================
# ----- DELETE EVERYTHING ABOVE THIS LINE -----
- title: "Hosted Runners on Linux operating system upgrade"
# The milestones for the deprecation announcement, and the removal.
removal_milestone: "17.0"
announcement_milestone: "16.10"
# Change breaking_change to false if needed.
breaking_change: true
# The stage and GitLab username of the person reporting the change,
# and a link to the deprecation issue
reporter: tmaczukin
stage: ci
issue_url: https://gitlab.com/gitlab-org/ci-cd/shared-runners/infrastructure/-/issues/60
impact: low # Can be one of: [critical, high, medium, low]
scope: instance # Can be one or a combination of: [instance, group, project]
resolution_role: Developer # Can be one of: [Admin, Owner, Maintainer, Developer]
manual_task: true # Can be true or false. Use this to denote whether a resolution action must be performed manually (true), or if it can be automated by using the API or other automation (false).
body: | # (required) Don't change this line.
With GitLab 17.0 we're upgrading the container-optimized operating system ([COS](https://cloud.google.com/container-optimized-os/docs))
of the ephemeral VMs used to execute jobs for [Hosted Runners on Linux](https://docs.gitlab.com/ee/ci/runners/saas/linux_saas_runner.html).
The COS upgrade includes a Docker Engine upgrade from Version 19.03.15 to Version 23.0.5, which introduced
a known compatibility issue.
GitLab CI/CD jobs [using Docker-in-Docker-based jobs](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker)
with a Docker-in-Docker service version prior to 20.10 and GitLab CI/CD jobs [using Kaniko to build container images](https://docs.gitlab.com/ee/ci/docker/using_kaniko.html)
with a Kaniko service version older than `v1.9.0` will start failing.
To fix these issues, an update of service version in `.gitlab-ci.yml` is required.
Both issues, including a detailed explanation of how they affect jobs and how to fix
the issue, are described
[in the announcement blog post](https://about.gitlab.com/blog/2023/10/04/updating-the-os-version-of-saas-runners-on-linux/).
# ==============================
# OPTIONAL END-OF-SUPPORT FIELDS
# ==============================
#
# If an End of Support period applies:
# 1) Share this announcement in the `#spt_managers` Support channel in Slack
# 2) Mention `@gitlab-com/support` in this merge request.
#
# When support for this feature ends, in XX.YY milestone format.
end_of_support_milestone:
# Array of tiers the feature is currently available to,
# like [Free, Silver, Gold, Core, Premium, Ultimate]
tiers:
# Links to documentation and thumbnail image
documentation_url:
image_url:
# Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
video_url:

View File

@ -1,10 +1,25 @@
---
table_name: dependency_proxy_blob_states
classes:
- Geo::DependencyProxyBlobState
- Geo::DependencyProxyBlobState
feature_categories:
- geo_replication
- geo_replication
description: Separate table for dependency proxy blob verification states
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101429
milestone: '15.6'
gitlab_schema: gitlab_main
gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
group_id:
references: namespaces
backfill_via:
parent:
foreign_key: dependency_proxy_blob_id
table: dependency_proxy_blobs
sharding_key: group_id
belongs_to: dependency_proxy_blob

View File

@ -1,10 +1,25 @@
---
table_name: dependency_proxy_manifest_states
classes:
- Geo::DependencyProxyManifestState
- Geo::DependencyProxyManifestState
feature_categories:
- geo_replication
- geo_replication
description: Separate table for dependency proxy manifest verification states
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102908
milestone: '15.6'
gitlab_schema: gitlab_main
gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
group_id:
references: namespaces
backfill_via:
parent:
foreign_key: dependency_proxy_manifest_id
table: dependency_proxy_manifests
sharding_key: group_id
belongs_to: dependency_proxy_manifest

View File

@ -7,4 +7,19 @@ feature_categories:
description: User mentions in epic descriptions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19009
milestone: '12.6'
gitlab_schema: gitlab_main
gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
group_id:
references: namespaces
backfill_via:
parent:
foreign_key: epic_id
table: epics
sharding_key: group_id
belongs_to: epic

View File

@ -25479,6 +25479,7 @@ Check permissions for the current user on a vulnerability finding.
| <a id="projectid"></a>`id` | [`ID!`](#id) | ID of the project. |
| <a id="projectimportstatus"></a>`importStatus` | [`String`](#string) | Status of import background job of the project. |
| <a id="projectincidentmanagementtimelineeventtags"></a>`incidentManagementTimelineEventTags` | [`[TimelineEventTagType!]`](#timelineeventtagtype) | Timeline event tags for the project. |
| <a id="projectisadjourneddeletionenabled"></a>`isAdjournedDeletionEnabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 16.11. **Status**: Experiment. Indicates if delayed project deletion is enabled. |
| <a id="projectiscatalogresource"></a>`isCatalogResource` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 15.11. **Status**: Experiment. Indicates if a project is a catalog resource. |
| <a id="projectisforked"></a>`isForked` | [`Boolean!`](#boolean) | Project is forked. |
| <a id="projectissuesaccesslevel"></a>`issuesAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for issues access. |
@ -25509,6 +25510,7 @@ Check permissions for the current user on a vulnerability finding.
| <a id="projectpackagesprotectionrules"></a>`packagesProtectionRules` | [`PackagesProtectionRuleConnection`](#packagesprotectionruleconnection) | Packages protection rules for the project. (see [Connections](#connections)) |
| <a id="projectpath"></a>`path` | [`String!`](#string) | Path of the project. |
| <a id="projectpathlocks"></a>`pathLocks` | [`PathLockConnection`](#pathlockconnection) | The project's path locks. (see [Connections](#connections)) |
| <a id="projectpermanentdeletiondate"></a>`permanentDeletionDate` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 16.11. **Status**: Experiment. Date when project will be deleted if delayed project deletion is enabled. |
| <a id="projectpipelineanalytics"></a>`pipelineAnalytics` | [`PipelineAnalytics`](#pipelineanalytics) | Pipeline analytics. |
| <a id="projectpipelinetriggers"></a>`pipelineTriggers` **{warning-solid}** | [`PipelineTriggerConnection`](#pipelinetriggerconnection) | **Introduced** in GitLab 16.3. **Status**: Experiment. List of pipeline trigger tokens. |
| <a id="projectpreventmergewithoutjiraissueenabled"></a>`preventMergeWithoutJiraIssueEnabled` | [`Boolean!`](#boolean) | Indicates if an associated issue from Jira is required. |

View File

@ -1241,33 +1241,6 @@ set `AUTO_DEVOPS_BUILD_IMAGE_CNB_BUILDER` to `heroku/builder:20`.
<div class="deprecation breaking-change" data-milestone="17.0">
### Hosted Runners on Linux operating system upgrade
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">16.10</span>
- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/ci-cd/shared-runners/infrastructure/-/issues/60).
</div>
With GitLab 17.0 we're upgrading the container-optimized operating system ([COS](https://cloud.google.com/container-optimized-os/docs))
of the ephemeral VMs used to execute jobs for [Hosted Runners on Linux](https://docs.gitlab.com/ee/ci/runners/saas/linux_saas_runner.html).
The COS upgrade includes a Docker Engine upgrade from Version 19.03.15 to Version 23.0.5, which introduced
a known compatibility issue.
GitLab CI/CD jobs [using Docker-in-Docker-based jobs](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker)
with a Docker-in-Docker service version prior to 20.10 and GitLab CI/CD jobs [using Kaniko to build container images](https://docs.gitlab.com/ee/ci/docker/using_kaniko.html)
with a Kaniko service version older than `v1.9.0` will start failing.
To fix these issues, an update of service version in `.gitlab-ci.yml` is required.
Both issues, including a detailed explanation of how they affect jobs and how to fix
the issue, are described
[in the announcement blog post](https://about.gitlab.com/blog/2023/10/04/updating-the-os-version-of-saas-runners-on-linux/).
</div>
<div class="deprecation breaking-change" data-milestone="17.0">
### Internal container registry API tag deletion endpoint
<div class="deprecation-notes">

View File

@ -122,7 +122,7 @@ module Gitlab
def instance_count_request(amount = 1)
@request_counter ||= Gitlab::Metrics.counter(:gitlab_redis_client_requests_total, 'Client side Redis request count, per Redis server')
@request_counter.increment({ storage: storage_key }, amount)
@request_counter.increment(storage_labels, amount)
end
def instance_count_pipelined_request(size)
@ -132,7 +132,7 @@ module Gitlab
{},
[10, 100, 1000, 10_000]
)
@pipeline_size_histogram.observe({ storage: storage_key, storage_shard: shard_key }, size)
@pipeline_size_histogram.observe(storage_labels, size)
end
def instance_count_exception(ex)
@ -140,12 +140,12 @@ module Gitlab
# server is doing. Redis itself does not expose error counts. This
# metric can be used for Redis alerting and service health monitoring.
@exception_counter ||= Gitlab::Metrics.counter(:gitlab_redis_client_exceptions_total, 'Client side Redis exception count, per Redis server, per exception class')
@exception_counter.increment({ storage: storage_key, storage_shard: shard_key, exception: ex.class.to_s })
@exception_counter.increment(storage_labels.merge(exception: ex.class.to_s))
end
def instance_count_connection_exception(ex)
@connection_exception_counter ||= Gitlab::Metrics.counter(:gitlab_redis_client_connection_exceptions_total, 'Client side Redis connection exception count, per Redis server, per exception class')
@connection_exception_counter.increment({ storage: storage_key, storage_shard: shard_key, exception: ex.class.to_s })
@connection_exception_counter.increment(storage_labels.merge(exception: ex.class.to_s))
end
def instance_count_cluster_redirection(ex)
@ -153,7 +153,7 @@ module Gitlab
# redirected to the right node, especially during resharding..
# This metric can be used for Redis alerting and service health monitoring.
@redirection_counter ||= Gitlab::Metrics.counter(:gitlab_redis_client_redirections_total, 'Client side Redis Cluster redirection count, per Redis node, per slot')
@redirection_counter.increment(decompose_redirection_message(ex.message).merge({ storage: storage_key, storage_shard: shard_key }))
@redirection_counter.increment(decompose_redirection_message(ex.message).merge(storage_labels))
end
def instance_observe_duration(duration)
@ -164,15 +164,19 @@ module Gitlab
[0.1, 0.5, 0.75, 1]
)
@request_latency_histogram.observe({ storage: storage_key, storage_shard: shard_key }, duration)
@request_latency_histogram.observe(storage_labels, duration)
end
def log_exception(ex)
::Gitlab::ErrorTracking.log_exception(ex, storage: storage_key, storage_shard: shard_key)
::Gitlab::ErrorTracking.log_exception(ex, **storage_labels)
end
private
def storage_labels
{ storage: storage_key, storage_shard: shard_key }
end
def request_count_key
strong_memoize(:request_count_key) { build_key(:redis_request_count) }
end

View File

@ -52758,7 +52758,7 @@ msgstr ""
msgid "TimeTracking|Time remaining: %{timeRemainingHumanReadable}"
msgstr ""
msgid "TimeTracking|Use /spend or /estimate to manage time."
msgid "TimeTracking|To manage time, use /spend or /estimate."
msgstr ""
msgid "Timeago|%s days ago"
@ -52999,6 +52999,9 @@ msgstr ""
msgid "To add a custom suffix, set up a Service Desk email address. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "To add active contacts, use /add_contacts."
msgstr ""
msgid "To add the entry manually, provide the following details to the application on your phone."
msgstr ""

View File

@ -86,13 +86,27 @@ describe('~/api/projects_api.js', () => {
jest.spyOn(axios, 'delete');
});
it('deletes to the correct URL', () => {
const expectedUrl = `/api/v7/projects/${projectId}`;
describe('without params', () => {
it('deletes to the correct URL', () => {
const expectedUrl = `/api/v7/projects/${projectId}`;
mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK);
mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK);
return projectsApi.deleteProject(projectId).then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl);
return projectsApi.deleteProject(projectId).then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { params: undefined });
});
});
});
describe('with params', () => {
it('deletes to the correct URL with params', () => {
const expectedUrl = `/api/v7/projects/${projectId}`;
mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK);
return projectsApi.deleteProject(projectId, { testParam: true }).then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { params: { testParam: true } });
});
});
});
});

View File

@ -9,6 +9,7 @@ import getIssueCrmContactsQuery from '~/sidebar/queries/get_issue_crm_contacts.q
import issueCrmContactsSubscription from '~/sidebar/queries/issue_crm_contacts.subscription.graphql';
import {
getIssueCrmContactsQueryResponse,
getIssueCrmContactsQueryResponseEmpty,
issueCrmContactsUpdateResponse,
issueCrmContactsUpdateNullResponse,
} from '../mock_data';
@ -21,6 +22,9 @@ describe('Issue crm contacts component', () => {
let fakeApollo;
const successQueryHandler = jest.fn().mockResolvedValue(getIssueCrmContactsQueryResponse);
const emptySuccessQueryHandler = jest
.fn()
.mockResolvedValue(getIssueCrmContactsQueryResponseEmpty);
const successSubscriptionHandler = jest.fn().mockResolvedValue(issueCrmContactsUpdateResponse);
const nullSubscriptionHandler = jest.fn().mockResolvedValue(issueCrmContactsUpdateNullResponse);
@ -80,6 +84,16 @@ describe('Issue crm contacts component', () => {
);
});
it('has an empty state', async () => {
mountComponent({
queryHandler: emptySuccessQueryHandler,
subscriptionHandler: nullSubscriptionHandler,
});
await waitForPromises();
expect(wrapper.findByTestId('crm-empty-message').exists()).toBe(true);
});
it('renders correct results after subscription update', async () => {
mountComponent();
await waitForPromises();

View File

@ -31,6 +31,18 @@ export const getIssueCrmContactsQueryResponse = {
},
};
export const getIssueCrmContactsQueryResponseEmpty = {
data: {
issue: {
__typename: 'Issue',
id: 'gid://gitlab/Issue/123',
customerRelationsContacts: {
nodes: [],
},
},
},
};
export const issueCrmContactsUpdateNullResponse = {
data: {
issueCrmContactsUpdated: null,

View File

@ -13,6 +13,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
@ -83,6 +84,7 @@ describe('Snippet header component', () => {
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
GlIcon,
GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }),
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
@ -99,9 +101,10 @@ describe('Snippet header component', () => {
const findSpamAction = () => wrapper.findByText('Submit as spam');
const findDeleteAction = () => wrapper.findByText('Delete');
const findDeleteModal = () => wrapper.findComponent(GlModal);
const findDeleteModalDeleteAction = () => wrapper.findByTestId('delete-snippet-button');
const findIcon = () => wrapper.findComponent(GlIcon);
const findTooltip = () => getBinding(findIcon().element, 'gl-tooltip');
const findSpamIcon = () => wrapper.findComponent('[data-testid="snippets-spam-icon"]');
const findSpamIcon = () => wrapper.findByTestId('snippets-spam-icon');
const title = 'The property of Thor';
@ -294,17 +297,21 @@ describe('Snippet header component', () => {
});
describe('Delete mutation', () => {
const deleteSnippet = async () => {
const openDeleteSnippetModal = async () => {
// Click delete action
findDropdown().trigger('click');
findDeleteAction().trigger('click');
await nextTick();
};
const deleteSnippet = async () => {
await openDeleteSnippetModal();
expect(findDeleteModal().props().visible).toBe(true);
// Click delete button in delete modal
document.querySelector('[data-testid="delete-snippet-button"').click();
findDeleteModalDeleteAction().trigger('click');
await waitForPromises();
};
@ -325,9 +332,19 @@ describe('Snippet header component', () => {
await deleteSnippet();
expect(document.querySelector('[data-testid="delete-alert"').textContent.trim()).toBe(
ERROR_MSG,
);
expect(wrapper.findByTestId('delete-alert').text()).toBe(ERROR_MSG);
});
it('puts the `Delete snippet` modal button in the loading state on click', async () => {
createComponent();
expect(findDeleteModalDeleteAction().props('loading')).toBe(false);
await openDeleteSnippetModal();
findDeleteModalDeleteAction().trigger('click');
await nextTick();
expect(findDeleteModalDeleteAction().props('loading')).toBe(true);
});
describe('in case of successful mutation, closes modal and redirects to correct listing', () => {

View File

@ -33,7 +33,7 @@ describe('WorkItemTimeTracking component', () => {
createComponent({ timeEstimate: 0, totalTimeSpent: 0 });
expect(findTimeTrackingBody().text()).toMatchInterpolatedText(
'Use /spend or /estimate to manage time.',
'To manage time, use /spend or /estimate.',
);
expect(findProgressBar().exists()).toBe(false);
});