diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index ccdc8cf259f..bc811e0504c 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-0c29d111e214eff897572517c13dcf3f524b3c19
+c6c944b8fd09e6fa3b9a607ffdea574a87a56d93
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index 7a9f89a0c87..0c393ca6b2f 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -75,6 +75,16 @@ export const MAX_DATE_RANGE_TEXT = (maxDateRange) => {
);
};
+// Limits the number of decimals we round values to
+export const MAX_METRIC_PRECISION = 4;
+
+export const UNITS = {
+ COUNT: 'COUNT',
+ DAYS: 'DAYS',
+ PER_DAY: 'PER_DAY',
+ PERCENT: 'PERCENT',
+};
+
export const NUMBER_OF_DAYS_SELECTED = (numDays) => {
return n__('1 day selected', '%d days selected', numDays);
};
@@ -134,8 +144,17 @@ export const AI_METRICS = {
DUO_CHAT_USAGE_RATE: 'duo_chat_usage_rate',
};
-export const METRIC_TOOLTIPS = {
+export const VALUE_STREAM_METRIC_DISPLAY_UNITS = {
+ [UNITS.COUNT]: '',
+ [UNITS.DAYS]: __('days'),
+ [UNITS.PER_DAY]: __('/day'),
+ [UNITS.PERCENT]: '%',
+};
+
+export const VALUE_STREAM_METRIC_TILE_METADATA = {
[DORA_METRICS.DEPLOYMENT_FREQUENCY]: {
+ label: s__('DORA4Metrics|Deployment frequency'),
+ unit: UNITS.PER_DAY,
description: s__(
'ValueStreamAnalytics|Average number of deployments to production per day. This metric measures how often value is delivered to end users.',
),
@@ -144,6 +163,8 @@ export const METRIC_TOOLTIPS = {
docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'deployment-frequency' }),
},
[DORA_METRICS.LEAD_TIME_FOR_CHANGES]: {
+ label: s__('DORA4Metrics|Lead time for changes'),
+ unit: UNITS.DAYS,
description: s__(
'ValueStreamAnalytics|The time to successfully deliver a commit into production. This metric reflects the efficiency of CI/CD pipelines.',
),
@@ -152,6 +173,8 @@ export const METRIC_TOOLTIPS = {
docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'lead-time-for-changes' }),
},
[DORA_METRICS.TIME_TO_RESTORE_SERVICE]: {
+ label: s__('DORA4Metrics|Time to restore service'),
+ unit: UNITS.DAYS,
description: s__(
'ValueStreamAnalytics|The time it takes an organization to recover from a failure in production.',
),
@@ -160,22 +183,27 @@ export const METRIC_TOOLTIPS = {
docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'time-to-restore-service' }),
},
[DORA_METRICS.CHANGE_FAILURE_RATE]: {
+ label: s__('DORA4Metrics|Change failure rate'),
description: s__(
'ValueStreamAnalytics|Percentage of deployments that cause an incident in production.',
),
groupLink: '-/analytics/ci_cd?tab=change-failure-rate',
projectLink: '-/pipelines/charts?chart=change-failure-rate',
docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'change-failure-rate' }),
+ unit: UNITS.PERCENT,
},
[FLOW_METRICS.LEAD_TIME]: {
+ label: s__('DORA4Metrics|Lead time'),
description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'),
groupLink: '-/analytics/value_stream_analytics',
projectLink: '-/value_stream_analytics',
docsLink: helpPagePath('user/group/value_stream_analytics/index', {
anchor: 'lifecycle-metrics',
}),
+ unit: UNITS.DAYS,
},
[FLOW_METRICS.CYCLE_TIME]: {
+ label: s__('DORA4Metrics|Cycle time'),
description: s__(
"ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.",
),
@@ -184,25 +212,39 @@ export const METRIC_TOOLTIPS = {
docsLink: helpPagePath('user/group/value_stream_analytics/index', {
anchor: 'lifecycle-metrics',
}),
+ unit: UNITS.DAYS,
},
[FLOW_METRICS.ISSUES]: {
+ label: s__('DORA4Metrics|Issues created'),
+ unit: UNITS.COUNT,
description: s__('ValueStreamAnalytics|Number of new issues created.'),
groupLink: '-/issues_analytics',
projectLink: '-/analytics/issues_analytics',
docsLink: helpPagePath('user/group/issues_analytics/index'),
},
+ [FLOW_METRICS.COMMITS]: {
+ label: s__('DORA4Metrics|Commits'),
+ unit: UNITS.COUNT,
+ description: s__('ValueStreamAnalytics|Number of commits pushed to the default branch'),
+ },
+ [FLOW_METRICS.DEPLOYS]: {
+ label: s__('DORA4Metrics|Deploys'),
+ unit: UNITS.COUNT,
+ description: s__('ValueStreamAnalytics|Total number of deploys to production.'),
+ groupLink: '-/analytics/productivity_analytics',
+ projectLink: '-/analytics/merge_request_analytics',
+ docsLink: helpPagePath('user/analytics/merge_request_analytics'),
+ },
+};
+
+export const VALUE_STREAM_METRIC_METADATA = {
+ ...VALUE_STREAM_METRIC_TILE_METADATA,
[FLOW_METRICS.ISSUES_COMPLETED]: {
description: s__('ValueStreamAnalytics|Number of issues closed by month.'),
groupLink: '-/issues_analytics',
projectLink: '-/analytics/issues_analytics',
docsLink: helpPagePath('user/group/issues_analytics/index'),
},
- [FLOW_METRICS.DEPLOYS]: {
- description: s__('ValueStreamAnalytics|Total number of deploys to production.'),
- groupLink: '-/analytics/productivity_analytics',
- projectLink: '-/analytics/merge_request_analytics',
- docsLink: helpPagePath('user/analytics/merge_request_analytics'),
- },
[CONTRIBUTOR_METRICS.COUNT]: {
description: s__(
'ValueStreamAnalytics|Number of monthly unique users with contributions in the group.',
@@ -254,42 +296,6 @@ export const METRIC_TOOLTIPS = {
},
};
-// TODO: Remove this once the migration to METRIC_TOOLTIPS is complete
-// https://gitlab.com/gitlab-org/gitlab/-/issues/388067
-export const METRICS_POPOVER_CONTENT = {
- lead_time: {
- description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'),
- },
- cycle_time: {
- description: s__(
- "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.",
- ),
- },
- lead_time_for_changes: {
- description: s__(
- 'ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period.',
- ),
- },
- issues: { description: s__('ValueStreamAnalytics|Number of new issues created.') },
- deploys: { description: s__('ValueStreamAnalytics|Total number of deploys to production.') },
- deployment_frequency: {
- description: s__('ValueStreamAnalytics|Average number of deployments to production per day.'),
- },
- commits: {
- description: s__('ValueStreamAnalytics|Number of commits pushed to the default branch'),
- },
- time_to_restore_service: {
- description: s__(
- 'ValueStreamAnalytics|Median time an incident was open on a production environment in the given time period.',
- ),
- },
- change_failure_rate: {
- description: s__(
- 'ValueStreamAnalytics|Percentage of deployments that cause an incident in production.',
- ),
- },
-};
-
export const USAGE_OVERVIEW_NO_DATA_ERROR = s__(
'ValueStreamAnalytics|Failed to load usage overview data',
);
diff --git a/app/assets/javascripts/analytics/shared/graphql/constants.js b/app/assets/javascripts/analytics/shared/graphql/constants.js
new file mode 100644
index 00000000000..dd913409276
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/graphql/constants.js
@@ -0,0 +1,2 @@
+export const BUCKETING_INTERVAL_ALL = 'ALL';
+export const BUCKETING_INTERVAL_MONTHLY = 'MONTHLY';
diff --git a/app/assets/javascripts/analytics/shared/graphql/dora_metric_item.fragment.graphql b/app/assets/javascripts/analytics/shared/graphql/dora_metric_item.fragment.graphql
new file mode 100644
index 00000000000..780dbcc32f9
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/graphql/dora_metric_item.fragment.graphql
@@ -0,0 +1,7 @@
+fragment DoraMetricItem on DoraMetric {
+ date
+ deployment_frequency: deploymentFrequency
+ change_failure_rate: changeFailureRate
+ lead_time_for_changes: leadTimeForChanges
+ time_to_restore_service: timeToRestoreService
+}
diff --git a/app/assets/javascripts/analytics/shared/graphql/dora_metrics.query.graphql b/app/assets/javascripts/analytics/shared/graphql/dora_metrics.query.graphql
new file mode 100644
index 00000000000..ae48c0117bb
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/graphql/dora_metrics.query.graphql
@@ -0,0 +1,25 @@
+#import "./dora_metric_item.fragment.graphql"
+
+query doraMetricsQuery(
+ $fullPath: ID!
+ $startDate: Date!
+ $endDate: Date!
+ $interval: DoraMetricBucketingInterval!
+) {
+ project(fullPath: $fullPath) {
+ id
+ dora {
+ metrics(startDate: $startDate, endDate: $endDate, interval: $interval) {
+ ...DoraMetricItem
+ }
+ }
+ }
+ group(fullPath: $fullPath) {
+ id
+ dora {
+ metrics(startDate: $startDate, endDate: $endDate, interval: $interval) {
+ ...DoraMetricItem
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/analytics/shared/graphql/flow_metric_item.fragment.graphql b/app/assets/javascripts/analytics/shared/graphql/flow_metric_item.fragment.graphql
new file mode 100644
index 00000000000..74756649ebf
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/graphql/flow_metric_item.fragment.graphql
@@ -0,0 +1,12 @@
+fragment FlowMetricItem on ValueStreamAnalyticsMetric {
+ unit
+ value
+ identifier
+ links {
+ label
+ name
+ docsLink
+ url
+ }
+ title
+}
diff --git a/app/assets/javascripts/analytics/shared/graphql/flow_metrics.query.graphql b/app/assets/javascripts/analytics/shared/graphql/flow_metrics.query.graphql
new file mode 100644
index 00000000000..0132ce7cbc0
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/graphql/flow_metrics.query.graphql
@@ -0,0 +1,58 @@
+#import "./flow_metric_item.fragment.graphql"
+
+query flowMetricsQuery($fullPath: ID!, $startDate: Time!, $endDate: Time!, $labelNames: [String!]) {
+ project(fullPath: $fullPath) {
+ id
+ flowMetrics {
+ issues: issueCount(from: $startDate, to: $endDate, labelNames: $labelNames) {
+ ...FlowMetricItem
+ }
+ issues_completed: issuesCompletedCount(
+ from: $startDate
+ to: $endDate
+ labelNames: $labelNames
+ ) {
+ ...FlowMetricItem
+ }
+ cycle_time: cycleTime(from: $startDate, to: $endDate, labelNames: $labelNames) {
+ ...FlowMetricItem
+ }
+ lead_time: leadTime(from: $startDate, to: $endDate, labelNames: $labelNames) {
+ ...FlowMetricItem
+ }
+ deploys: deploymentCount(from: $startDate, to: $endDate) {
+ ...FlowMetricItem
+ }
+ median_time_to_merge: timeToMerge(from: $startDate, to: $endDate) {
+ ...FlowMetricItem
+ }
+ }
+ }
+ group(fullPath: $fullPath) {
+ id
+ flowMetrics {
+ issues: issueCount(from: $startDate, to: $endDate, labelNames: $labelNames) {
+ ...FlowMetricItem
+ }
+ issues_completed: issuesCompletedCount(
+ from: $startDate
+ to: $endDate
+ labelNames: $labelNames
+ ) {
+ ...FlowMetricItem
+ }
+ cycle_time: cycleTime(from: $startDate, to: $endDate, labelNames: $labelNames) {
+ ...FlowMetricItem
+ }
+ lead_time: leadTime(from: $startDate, to: $endDate, labelNames: $labelNames) {
+ ...FlowMetricItem
+ }
+ deploys: deploymentCount(from: $startDate, to: $endDate) {
+ ...FlowMetricItem
+ }
+ median_time_to_merge: timeToMerge(from: $startDate, to: $endDate) {
+ ...FlowMetricItem
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js
index 99a9f05cd5d..98ad20b9b14 100644
--- a/app/assets/javascripts/analytics/shared/utils.js
+++ b/app/assets/javascripts/analytics/shared/utils.js
@@ -3,7 +3,7 @@ import dateFormat from '~/lib/dateformat';
import { slugify } from '~/lib/utils/text_utility';
import { joinPaths } from '~/lib/utils/url_utility';
import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-import { dateFormats, METRICS_POPOVER_CONTENT } from './constants';
+import { dateFormats, VALUE_STREAM_METRIC_METADATA } from './constants';
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
if (!searchTerm?.length) return data;
@@ -117,7 +117,7 @@ const requestData = ({ request, endpoint, requestPath, params, name }) => {
export const fetchMetricsData = (requests = [], requestPath, params) => {
const promises = requests.map((r) => requestData({ ...r, requestPath, params }));
return Promise.all(promises).then((responses) =>
- prepareTimeMetricsData(flatten(responses), METRICS_POPOVER_CONTENT),
+ prepareTimeMetricsData(flatten(responses), VALUE_STREAM_METRIC_METADATA),
);
};
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
index a599a8bf35c..9ce061157bf 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
@@ -229,7 +229,9 @@ export default {
"
>
- {{ content }}
+ {{
+ content
+ }}
diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
index cf9794e6a87..8cd150279c0 100644
--- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
+++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
@@ -1,3 +1,5 @@
+import { EMOJI_VERSION } from '~/emoji';
+
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
@@ -72,6 +74,14 @@ function isPersonZwjEmoji(emojiUnicode) {
return hasPersonEmoji && hasZwj;
}
+// If the backend emoji support is newer, then there may already be emojis in use
+// that were not "supported" before but were displayable. In that scenario, we want to
+// allow those emojis to be recognized and displayed, until the frontend (usually in the
+// following release) is updated.
+function isBackendEmojiNewer() {
+ return EMOJI_VERSION < gon.emoji_backend_version;
+}
+
// Helper so we don't have to run `isFlagEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
@@ -119,7 +129,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
// For comments about each scenario, see the comments above each individual respective function
return (
- unicodeSupportMap[unicodeVersion] &&
+ (unicodeSupportMap[unicodeVersion] || isBackendEmojiNewer()) &&
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
diff --git a/app/assets/javascripts/glql/components/presenters/list.vue b/app/assets/javascripts/glql/components/presenters/list.vue
index 84d5603ac53..ed52e1200a8 100644
--- a/app/assets/javascripts/glql/components/presenters/list.vue
+++ b/app/assets/javascripts/glql/components/presenters/list.vue
@@ -1,10 +1,15 @@
@@ -43,16 +54,23 @@ export default {
:key="itemIndex"
:data-testid="`list-item-${itemIndex}`"
>
-
+
-
- {{ __('No data found for this query') }}
-
+
+ {{ __('No data found for this query') }}
+
- {{ __('Generated by GLQL') }}
+
+
+
+
+ {{ content }}
+
+
+
diff --git a/app/assets/javascripts/glql/components/presenters/table.vue b/app/assets/javascripts/glql/components/presenters/table.vue
index 326455c7cb1..bfb941b5d2f 100644
--- a/app/assets/javascripts/glql/components/presenters/table.vue
+++ b/app/assets/javascripts/glql/components/presenters/table.vue
@@ -1,5 +1,7 @@
@@ -72,11 +84,18 @@ export default {
- {{ __('No data found for this query') }}
+ {{ __('No data found for this query') }}
- {{ __('Generated by GLQL') }}
+
+
+
+
+ {{ content }}
+
+
+
diff --git a/app/assets/javascripts/integrations/beyond_identity/components/exclusions_list.vue b/app/assets/javascripts/integrations/beyond_identity/components/exclusions_list.vue
index 47cc8f45aff..eacd83c7959 100644
--- a/app/assets/javascripts/integrations/beyond_identity/components/exclusions_list.vue
+++ b/app/assets/javascripts/integrations/beyond_identity/components/exclusions_list.vue
@@ -201,7 +201,7 @@ export default {
class="gl-border-b gl-flex gl-items-center gl-justify-between gl-bg-gray-10 gl-p-4 gl-py-5"
>
{{ $options.i18n.helpText }}
- {{
+ {{
$options.i18n.addExclusions
}}
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index 288b8ffc90b..25d4588f97c 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -278,19 +278,25 @@ export default {
v-if="showSaveAndAdd"
variant="confirm"
category="secondary"
+ data-testid="save-and-add-button"
:disabled="!isTimelineTextValid"
:loading="isEventProcessed"
@click="handleSave(true)"
>
{{ $options.i18n.saveAndAdd }}
-
+
{{ $options.i18n.cancel }}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue
index af770e92d18..2af48f4bcc5 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue
@@ -170,6 +170,7 @@ export default {
{{ s__('ContainerRegistry|Add rule') }}
@@ -323,6 +324,7 @@ export default {
:title="__('Delete')"
:aria-label="__('Delete')"
:disabled="isProtectionRuleDeleteButtonDisabled(item)"
+ data-testid="delete-btn"
@click="showProtectionRuleDeletionConfirmModal(item)"
/>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
index 99e6157bb4c..26e9c51c84a 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
@@ -203,6 +203,7 @@ export default {
type="submit"
:disabled="isSubmitButtonDisabled"
:loading="showLoadingIcon"
+ data-testid="add-rule-btn"
>{{ s__('PackageRegistry|Add rule') }}
{{ __('Cancel') }}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
index 672423b0aa3..f6127a64e88 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
@@ -305,6 +305,7 @@ export default {
:aria-label="$options.i18n.minimumAccessLevelForPush"
:options="minimumAccessLevelOptions"
:disabled="isProtectionRuleMinimumAccessLevelFormSelectDisabled(item)"
+ data-testid="push-access-select"
@change="updatePackageProtectionRule(item)"
/>
@@ -317,6 +318,7 @@ export default {
icon="remove"
:title="__('Delete')"
:aria-label="__('Delete')"
+ data-testid="delete-rule-btn"
:disabled="isProtectionRuleDeleteButtonDisabled(item)"
@click="showProtectionRuleDeletionConfirmModal(item)"
/>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/rule_drawer.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/rule_drawer.vue
index f13671952ff..156c7fc42b0 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/rule_drawer.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/rule_drawer.vue
@@ -250,7 +250,12 @@ export default {
>
{{ $options.i18n.saveChanges }}
-
+
{{ $options.i18n.cancel }}
diff --git a/app/assets/javascripts/projects/your_work/components/app.vue b/app/assets/javascripts/projects/your_work/components/app.vue
index 3c1253d333a..868376e5784 100644
--- a/app/assets/javascripts/projects/your_work/components/app.vue
+++ b/app/assets/javascripts/projects/your_work/components/app.vue
@@ -222,9 +222,13 @@ export default {
{{ tab.text }}
- {{
- numberToMetricPrefix(tabCount(tab))
- }}
+ {{ numberToMetricPrefix(tabCount(tab)) }}
diff --git a/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue b/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue
index d17144669fe..6e855b09cd0 100644
--- a/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue
+++ b/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue
@@ -66,12 +66,14 @@ export default {
navigate($options.tagRefType)"
>{{ $options.i18n.viewTagButton }}
navigate($options.branchRefType)"
>{{ $options.i18n.viewBranchButton }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 9a39fd3e66c..0c5f3a1b9b3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -617,11 +617,10 @@ export default {
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue
index 4919ad7f5ef..1344174cad4 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue
+++ b/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue
@@ -66,6 +66,7 @@ export default {
icon="remove"
:aria-label="deleteButtonLabel"
category="tertiary"
+ data-testid="delete-group-btn"
@click="$emit('delete', data.id)"
/>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue
index 9ffe0a6b038..6d66072c879 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue
+++ b/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue
@@ -53,6 +53,7 @@ export default {
icon="remove"
:aria-label="deleteButtonLabel"
category="tertiary"
+ data-testid="delete-user-btn"
@click="$emit('delete', data.id)"
/>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 56c09de9ac8..e4451df1387 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -594,6 +594,7 @@ export default {
v-model="findAndReplace.find"
:placeholder="__('Find')"
autofocus
+ data-testid="find-btn"
@keydown="handleKeyDown"
/>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index 88bef183397..689089f1312 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -378,6 +378,7 @@ export default {
:href="starsHref"
:aria-label="$options.i18n.stars"
class="gl-text-secondary"
+ data-testid="stars-btn"
>
{{ starCount }}
@@ -388,6 +389,7 @@ export default {
:href="forksHref"
:aria-label="$options.i18n.forks"
class="gl-text-secondary"
+ data-testid="forks-btn"
>
{{ forksCount }}
@@ -398,6 +400,7 @@ export default {
:href="mergeRequestsHref"
:aria-label="$options.i18n.mergeRequests"
class="gl-text-secondary"
+ data-testid="mrs-btn"
>
{{ openMergeRequestsCount }}
@@ -408,6 +411,7 @@ export default {
:href="issuesHref"
:aria-label="$options.i18n.issues"
class="gl-text-secondary"
+ data-testid="issues-btn"
>
{{ openIssuesCount }}
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue
index fb109c70dfe..d2e9cb4aede 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue
@@ -142,7 +142,7 @@ export default {
- {{ $options.i18n.close }}
+ {{ $options.i18n.close }}
{{ $options.i18n.deployRunnerInAws }}
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index fb90751c4bf..58948fe2a39 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -8,7 +8,6 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
-import { escapeRegExp } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { STATUS_OPEN, STATUS_CLOSED } from '~/issues/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
@@ -21,7 +20,12 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import WorkItemPrefetch from '~/work_items/components/work_item_prefetch.vue';
import { STATE_OPEN, STATE_CLOSED, LINKED_CATEGORIES_MAP } from '~/work_items/constants';
-import { isAssigneesWidget, isLabelsWidget, findLinkedItemsWidget } from '~/work_items/utils';
+import {
+ isAssigneesWidget,
+ isLabelsWidget,
+ findLinkedItemsWidget,
+ canRouterNav,
+} from '~/work_items/utils';
export default {
components: {
@@ -296,12 +300,14 @@ export default {
if (!this.fullPath) {
visitUrl(this.issuableLinkHref);
}
- const escapedFullPath = escapeRegExp(this.fullPath);
- // eslint-disable-next-line no-useless-escape
- const regex = new RegExp(`groups\/${escapedFullPath}\/-\/(work_items|epics)\/\\d+`);
- const isWorkItemPath = regex.test(this.issuableLinkHref);
+ const shouldRouterNav = canRouterNav({
+ fullPath: this.fullPath,
+ webUrl: this.issuableLinkHref,
+ isGroup: this.isGroup,
+ issueAsWorkItem: this.issueAsWorkItem,
+ });
- if (isWorkItemPath || this.issueAsWorkItem) {
+ if (shouldRouterNav) {
this.$router.push({
name: 'workItem',
params: {
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
index 27562750dae..0b533002a5a 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -10,7 +10,6 @@ import {
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
-import { escapeRegExp } from 'lodash';
import { __, s__, sprintf } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -18,7 +17,7 @@ import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/w
import RichTimestampTooltip from '../rich_timestamp_tooltip.vue';
import WorkItemTypeIcon from '../work_item_type_icon.vue';
import WorkItemStateBadge from '../work_item_state_badge.vue';
-import { findLinkedItemsWidget, getDisplayReference } from '../../utils';
+import { canRouterNav, findLinkedItemsWidget, getDisplayReference } from '../../utils';
import {
STATE_OPEN,
WIDGET_TYPE_ASSIGNEES,
@@ -165,12 +164,16 @@ export default {
if (e.metaKey || e.ctrlKey) {
return;
}
- const escapedFullPath = escapeRegExp(this.workItemFullPath);
- // eslint-disable-next-line no-useless-escape
- const regex = new RegExp(`groups\/${escapedFullPath}\/-\/(work_items|epics)\/\\d+`);
- const isWorkItemPath = regex.test(workItem.webUrl);
+ const shouldDefaultNavigate =
+ this.preventRouterNav ||
+ !canRouterNav({
+ fullPath: this.workItemFullPath,
+ webUrl: workItem.webUrl,
+ isGroup: this.isGroup,
+ issueAsWorkItem: this.issueAsWorkItem,
+ });
- if (!(isWorkItemPath || this.issueAsWorkItem) || this.preventRouterNav) {
+ if (shouldDefaultNavigate) {
this.$emit('click', e);
} else {
e.preventDefault();
diff --git a/app/assets/javascripts/work_items/components/work_item_drawer.vue b/app/assets/javascripts/work_items/components/work_item_drawer.vue
index 54a863398fd..f975a9b9f38 100644
--- a/app/assets/javascripts/work_items/components/work_item_drawer.vue
+++ b/app/assets/javascripts/work_items/components/work_item_drawer.vue
@@ -1,6 +1,5 @@
```
+
+## Quarantine list
+
+The `scripts/frontend/quarantined_vue3_specs.txt` file is built up of all the known failing Vue 3 test files.
+In order to not overwhelm us with failing pipelines, these files are skipped on the Vue 3 test job.
+
+If you're reading this, it's likely you were sent here by a failing quarantine job.
+This job is confusing as it fails when a test passes and it passes if they all fail.
+The reason for this is because all newly passing tests should be [removed from the quarantine list](#removing-from-the-quarantine-list).
+Congratulate yourself on fixing a previously failing test and remove it fom the quarantine list to get this pipeline passing again.
+
+### Removing from the quarantine list
+
+If your pipeline is failing because of the `vue3 check quarantined` jobs, good news!
+You fixed a previously failing test!
+What you need to do now is remove the newly-passing test from the quarantine list.
+This ensures that the test will continue to pass and prevent any further regressions.
+
+### Adding to the quarantine list
+
+Don't do it.
+This list should only get smaller, not larger.
+If your MR introduces a new test file or breaks a currently passing one, then you should fix it.
+
+If you are moving a test file from one location to another, then it's okay to modify the location in the quarantine list.
+However, before doing so, consider fixing the test first.
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 026a0f58b58..605173783e7 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -59,29 +59,6 @@ target branches.
Detected vulnerabilities appear in [merge requests](../index.md#merge-request), the [pipeline security tab](../index.md#pipeline-security-tab),
and the [vulnerability report](../index.md#vulnerability-report).
-1. To see all vulnerabilities detected, either:
- - From your project, select **Security & Compliance**, then **Vulnerability report**.
- - From your pipeline, select the **Security** tab.
- - From the merge request, go to the **Security scanning** widget and select **Full report** tab.
-
-1. Select a DAST vulnerability's description. The following fields are examples of what a DAST analyzer may produce to aid investigation and rectification of the underlying cause. Each analyzer may output different fields.
-
- | Field | Description |
- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------ |
- | Description | Description of the vulnerability. |
- | Evidence | Evidence of the data found that verified the vulnerability. Often a snippet of the request or response, this can be used to help verify that the finding is a vulnerability. |
- | Identifiers | Identifiers of the vulnerability. |
- | Links | Links to further details of the detected vulnerability. |
- | Method | HTTP method used to detect the vulnerability. |
- | Project | Namespace and project in which the vulnerability was detected. |
- | Request Headers | Headers of the request. |
- | Response Headers | Headers of the response received from the application. |
- | Response Status | Response status received from the application. |
- | Scanner Type | Type of vulnerability report. |
- | Severity | Severity of the vulnerability. |
- | Solution | Details of a recommended solution to the vulnerability. |
- | URL | URL at which the vulnerability was detected. |
-
NOTE:
A pipeline may consist of multiple jobs, including SAST and DAST scanning. If any job
fails to finish for any reason, the security dashboard doesn't show DAST scanner output. For
diff --git a/doc/user/custom_roles/abilities.md b/doc/user/custom_roles/abilities.md
index 1a71124c4a9..4b0375a69be 100644
--- a/doc/user/custom_roles/abilities.md
+++ b/doc/user/custom_roles/abilities.md
@@ -23,6 +23,12 @@ Some permissions require having other permissions enabled first. For example, ad
These requirements are documented in the `Required permission` column in the following table.
+## Admin
+
+| Name | Required permission | Description | Introduced in | Feature flag | Enabled in |
+|:-----|:------------|:------------------|:---------|:--------------|:---------|
+| [`read_admin_dashboard`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171581) | | Read-only access to admin dashboard | GitLab [17.6](https://gitlab.com/gitlab-org/gitlab/-/issues/501549) | | |
+
## Code review workflow
| Name | Required permission | Description | Introduced in | Feature flag | Enabled in |
diff --git a/doc/user/group/import/migrated_items.md b/doc/user/group/import/migrated_items.md
index 794fa199695..590ffabe38e 100644
--- a/doc/user/group/import/migrated_items.md
+++ b/doc/user/group/import/migrated_items.md
@@ -65,10 +65,14 @@ Group items that are migrated to the destination GitLab instance include:
### Excluded items
-Some group items are excluded from migration because they either:
+Some group items are excluded from migration because they:
-- May contain sensitive information: CI/CD variables, webhooks, and deploy tokens.
-- Are not supported: push rules.
+- Might contain sensitive information:
+ - CI/CD variables
+ - Deploy tokens
+ - Webhooks
+- Are not supported:
+ - Push rules
## Migrated project items
@@ -195,13 +199,15 @@ Setting-related project items that are migrated to the destination GitLab instan
### Excluded items
-Some project items are excluded from migration because they either:
+Some project items are excluded from migration because they:
-- May contain sensitive information:
+- Might contain sensitive information:
- CI/CD variables
- CI/CD job logs
+ - Container registry images
- Deploy keys
- Deploy tokens
+ - Encrypted tokens
- Job artifacts
- Pipeline schedule variables
- Pipeline triggers
@@ -209,10 +215,10 @@ Some project items are excluded from migration because they either:
- Are not supported:
- Agents
- Approval rules
- - Container Registry
+ - Container registry
- Environments
- Feature flags
- - Infrastructure Registry
+ - Infrastructure registry
- Package registry
- Pages domains
- Remote mirrors
diff --git a/lib/gitlab/background_migration/delete_orphaned_partitioned_ci_runner_machine_records.rb b/lib/gitlab/background_migration/delete_orphaned_partitioned_ci_runner_machine_records.rb
new file mode 100644
index 00000000000..fe63afb5f94
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_orphaned_partitioned_ci_runner_machine_records.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ class DeleteOrphanedPartitionedCiRunnerMachineRecords < BatchedMigrationJob
+ operation_name :delete_orphaned_partitioned_ci_runner_machine_records
+ feature_category :fleet_visibility
+
+ class CiRunner < ::Ci::ApplicationRecord
+ self.table_name = :ci_runners_e59bb2812d
+ self.primary_key = :id
+ end
+
+ def perform
+ distinct_each_batch do |batch|
+ runner_ids = batch.pluck(batch_column)
+ runner_query = CiRunner
+ .where('ci_runner_machines_687967fa8a.runner_id = ci_runners_e59bb2812d.id')
+ .where('ci_runner_machines_687967fa8a.runner_type = ci_runners_e59bb2812d.runner_type')
+ .select(1)
+
+ base_relation
+ .where(batch_column => runner_ids)
+ .where('NOT EXISTS (?)', runner_query)
+ .delete_all
+ end
+ end
+
+ private
+
+ def base_relation
+ define_batchable_model(batch_table, connection: connection, primary_key: :id)
+ .where(batch_column => start_id..end_id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 6e5678ed920..1908b727d68 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -43,6 +43,7 @@ module Gitlab
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
gon.emoji_sprites_css_path = universal_path_to_stylesheet('emoji_sprites')
+ gon.emoji_backend_version = Gitlab::Emoji::EMOJI_VERSION
gon.gridstack_css_path = universal_path_to_stylesheet('lazy_bundles/gridstack')
gon.test_env = Rails.env.test?
gon.disable_animations = Gitlab.config.gitlab['disable_animations']
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1e2e58f9c32..41ad6de0f97 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -17334,6 +17334,9 @@ msgstr ""
msgid "DORA4Metrics|Change failure rate (percentage)"
msgstr ""
+msgid "DORA4Metrics|Commits"
+msgstr ""
+
msgid "DORA4Metrics|Contributor count"
msgstr ""
@@ -24262,7 +24265,7 @@ msgstr ""
msgid "Generate site and private keys at"
msgstr ""
-msgid "Generated by GLQL"
+msgid "Generated by %{linkStart}GLQL%{linkEnd}"
msgstr ""
msgid "Generated files are collapsed by default. To change this behavior, edit the %{tagStart}.gitattributes%{tagEnd} file. %{linkStart}Learn more.%{linkEnd}"
@@ -60760,9 +60763,6 @@ msgstr[1] ""
msgid "ValueStreamAnalytics|<1 minute"
msgstr ""
-msgid "ValueStreamAnalytics|Average number of deployments to production per day."
-msgstr ""
-
msgid "ValueStreamAnalytics|Average number of deployments to production per day. This metric measures how often value is delivered to end users."
msgstr ""
@@ -60784,15 +60784,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Lifecycle metrics"
msgstr ""
-msgid "ValueStreamAnalytics|Median time an incident was open on a production environment in the given time period."
-msgstr ""
-
msgid "ValueStreamAnalytics|Median time between merge request created and merge request merged."
msgstr ""
-msgid "ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period."
-msgstr ""
-
msgid "ValueStreamAnalytics|Median time from issue created to issue closed."
msgstr ""
diff --git a/package.json b/package.json
index 420b9a674c0..c4234608219 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
"@gitlab/duo-ui": "^2.0.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
- "@gitlab/query-language": "^0.0.5-a-20241105",
+ "@gitlab/query-language": "^0.0.5-a-20241112",
"@gitlab/svgs": "3.121.0",
"@gitlab/ui": "102.1.0",
"@gitlab/vue-router-vue3": "npm:vue-router@4.1.6",
diff --git a/scripts/frontend/check_jest_vue3_quarantine.js b/scripts/frontend/check_jest_vue3_quarantine.js
index be9e4412434..162257f4d86 100644
--- a/scripts/frontend/check_jest_vue3_quarantine.js
+++ b/scripts/frontend/check_jest_vue3_quarantine.js
@@ -76,6 +76,9 @@ function section(header, callback, { showCollapsed = true } = {}) {
}
function reportPassingSpecsShouldBeUnquarantined(passed) {
+ const docsLink =
+ // eslint-disable-next-line no-restricted-syntax
+ 'https://docs.gitlab.com/ee/development/testing_guide/testing_vue3.html#quarantine-list';
console.warn(' ');
console.warn(
`The following ${passed.length} spec file(s) now pass(es) under Vue 3, and so must be removed from quarantine:`,
@@ -88,6 +91,7 @@ function reportPassingSpecsShouldBeUnquarantined(passed) {
`To fix this job, remove the file(s) listed above from the file ${chalk.underline('scripts/frontend/quarantined_vue3_specs.txt')}.`,
),
);
+ console.warn(`For more information, please see ${docsLink}.`);
}
async function changedFiles() {
diff --git a/scripts/frontend/quarantined_vue3_specs.txt b/scripts/frontend/quarantined_vue3_specs.txt
index 70c40f4b53e..f06d046015f 100644
--- a/scripts/frontend/quarantined_vue3_specs.txt
+++ b/scripts/frontend/quarantined_vue3_specs.txt
@@ -205,7 +205,6 @@ spec/frontend/ci/pipelines_page/components/pipeline_multi_actions_spec.js
spec/frontend/ci/pipelines_page/components/pipelines_artifacts_spec.js
spec/frontend/ci/pipelines_page/components/pipelines_filtered_search_spec.js
spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
-spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
spec/frontend/ci/runner/components/runner_details_spec.js
spec/frontend/ci/runner/components/runner_form_fields_spec.js
spec/frontend/ci/runner/components/runner_managers_table_spec.js
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper.js b/spec/frontend/__helpers__/vue_test_utils_helper.js
index 6b712b6592b..0bd982efa5d 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper.js
@@ -9,20 +9,6 @@ import {
} from '@vue/test-utils';
import { compose } from 'lodash/fp';
-/**
- * Create a VTU wrapper from an element.
- *
- * If a Vue instance manages the element, the wrapper is created
- * with that Vue instance.
- *
- * @param {HTMLElement} element
- * @param {Object} options
- * @returns {Wrapper} VTU wrapper
- */
-const createWrapperFromElement = (element, options) =>
- // eslint-disable-next-line no-underscore-dangle
- createWrapper(element.__vue__ || element, options || {});
-
/**
* Query function type
* @callback FindFunction
@@ -154,7 +140,7 @@ export const extendedWrapper = (wrapper) => {
if (!elements.length) {
return new ErrorWrapper(query);
}
- return createWrapperFromElement(elements[0], this.options);
+ return createWrapper(elements[0], this.options);
},
},
};
@@ -169,7 +155,7 @@ export const extendedWrapper = (wrapper) => {
const elements = testingLibrary[`queryAll${query}`](this.element, text, options);
const wrappers = elements.map((element) => {
- const elementWrapper = createWrapperFromElement(element, this.options);
+ const elementWrapper = createWrapper(element, this.options);
elementWrapper.selector = text;
return elementWrapper;
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
index dada937d3bf..493e5f90c9c 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
@@ -140,12 +140,10 @@ describe('Vue test utils helpers', () => {
const text = 'foo bar';
const options = { selector: 'div' };
const mockDiv = document.createElement('div');
- let mockVm;
let wrapper;
beforeEach(() => {
jest.spyOn(vtu, 'createWrapper');
- mockVm = new Vue({ render: (h) => h('div') }).$mount();
wrapper = extendedWrapper(
shallowMount({
@@ -180,19 +178,6 @@ describe('Vue test utils helpers', () => {
});
});
- describe('when a Vue instance element is found', () => {
- beforeEach(() => {
- jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockVm.$el]);
- });
-
- it('returns a VTU wrapper', () => {
- const result = wrapper[findMethod](text, options);
-
- expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options);
- expect(result).toBeInstanceOf(VTUWrapper);
- expect(result.vm).toBeInstanceOf(Vue);
- });
- });
describe('when multiple elements are found', () => {
beforeEach(() => {
const mockSpan = document.createElement('span');
@@ -208,23 +193,6 @@ describe('Vue test utils helpers', () => {
});
});
- describe('when multiple Vue instances are found', () => {
- beforeEach(() => {
- const mockVm2 = new Vue({ render: (h) => h('span') }).$mount();
- jest
- .spyOn(testingLibrary, expectedQuery)
- .mockImplementation(() => [mockVm.$el, mockVm2.$el]);
- });
-
- it('returns the first element as a VTU wrapper', () => {
- const result = wrapper[findMethod](text, options);
-
- expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options);
- expect(result).toBeInstanceOf(VTUWrapper);
- expect(result.vm).toBeInstanceOf(Vue);
- });
- });
-
describe('when element is not found', () => {
beforeEach(() => {
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => []);
diff --git a/spec/frontend/analytics/cycle_analytics/components/value_stream_metrics_spec.js b/spec/frontend/analytics/cycle_analytics/components/value_stream_metrics_spec.js
index 83bf76b158f..099d6cc967a 100644
--- a/spec/frontend/analytics/cycle_analytics/components/value_stream_metrics_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/value_stream_metrics_spec.js
@@ -5,7 +5,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
-import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
+import { VSA_METRICS_GROUPS, VALUE_STREAM_METRIC_METADATA } from '~/analytics/shared/constants';
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
import ValueStreamsDashboardLink from '~/analytics/shared/components/value_streams_dashboard_link.vue';
@@ -91,7 +91,10 @@ describe('ValueStreamMetrics', () => {
});
describe('filterFn', () => {
- const transferredMetricsData = prepareTimeMetricsData(metricsData, METRICS_POPOVER_CONTENT);
+ const transferredMetricsData = prepareTimeMetricsData(
+ metricsData,
+ VALUE_STREAM_METRIC_METADATA,
+ );
it('with a filter function, will call the function with the metrics data', async () => {
const filteredData = [
diff --git a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
index a916a715b44..0967ebd4874 100644
--- a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
@@ -142,7 +142,7 @@ describe('RegistrationInstructions', () => {
expect(findPlatformsDrawer().props('open')).toBe(false);
- await findByText('How do I install GitLab Runner?').vm.$emit('click');
+ await wrapper.findByTestId('how-to-install-btn').vm.$emit('click');
expect(findPlatformsDrawer().props('open')).toBe(true);
await findPlatformsDrawer().vm.$emit('close');
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index 4d05449280a..64f4d395201 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -527,6 +527,10 @@ describe('emoji', () => {
});
describe('isEmojiUnicodeSupported', () => {
+ beforeEach(() => {
+ gon.emoji_backend_version = EMOJI_VERSION;
+ });
+
it('should gracefully handle empty string with unicode support', () => {
const isSupported = isEmojiUnicodeSupported({ '1.0': true }, '', '1.0');
@@ -536,7 +540,7 @@ describe('emoji', () => {
it('should gracefully handle empty string without unicode support', () => {
const isSupported = isEmojiUnicodeSupported({}, '', '1.0');
- expect(isSupported).toBeUndefined();
+ expect(isSupported).toBe(false);
});
it('bomb(6.0) with 6.0 support', () => {
@@ -575,6 +579,32 @@ describe('emoji', () => {
expect(isSupported).toBe(false);
});
+ it('bomb(6.0) without 6.0 but with backend support', () => {
+ gon.emoji_backend_version = EMOJI_VERSION + 1;
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = emptySupportMap;
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+
+ expect(isSupported).toBe(true);
+ });
+
+ it('bomb(6.0) without 6.0 with empty backend version', () => {
+ gon.emoji_backend_version = null;
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = emptySupportMap;
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+
+ expect(isSupported).toBe(false);
+ });
+
it('construction_worker_tone5(8.0) without skin tone modifier support', () => {
const emojiKey = 'construction_worker_tone5';
const unicodeSupportMap = {
diff --git a/spec/frontend/glql/components/presenters/list_spec.js b/spec/frontend/glql/components/presenters/list_spec.js
index b7390b4c5d5..c64cd2bcf80 100644
--- a/spec/frontend/glql/components/presenters/list_spec.js
+++ b/spec/frontend/glql/components/presenters/list_spec.js
@@ -50,9 +50,9 @@ describe('ListPresenter', () => {
expect(htmlPresenter1.props('data')).toBe(MOCK_ISSUES.nodes[0].description);
expect(htmlPresenter2.props('data')).toBe(MOCK_ISSUES.nodes[1].description);
- expect(listItem1.text()).toEqual('Issue 1 (#1) - @foobar - Open - This is a description');
+ expect(listItem1.text()).toEqual('Issue 1 (#1) · @foobar · Open · This is a description');
expect(listItem2.text()).toEqual(
- 'Issue 2 (#2 - closed) - @janedoe - Closed - This is another description',
+ 'Issue 2 (#2 - closed) · @janedoe · Closed · This is another description',
);
});
diff --git a/spec/frontend/glql/core/parser/query_spec.js b/spec/frontend/glql/core/parser/query_spec.js
index 07715c40d09..ad6caff68f3 100644
--- a/spec/frontend/glql/core/parser/query_spec.js
+++ b/spec/frontend/glql/core/parser/query_spec.js
@@ -20,7 +20,7 @@ describe('GLQL Query Parser', () => {
const result = await parseQuery(query, config);
expect(prettify(result)).toMatchInlineSnapshot(`
-"{
+"query GLQL {
issues(assigneeUsernames: "foobar", first: 50) {
nodes {
id
@@ -57,7 +57,7 @@ describe('GLQL Query Parser', () => {
const result = await parseQuery(query, config);
expect(prettify(result)).toMatchInlineSnapshot(`
-"{
+"query GLQL {
issues(
assigneeUsernames: "foobar"
or: {labelNames: ["bug", "feature"]}
diff --git a/spec/frontend/integrations/beyond_identity/components/exclusions_list_spec.js b/spec/frontend/integrations/beyond_identity/components/exclusions_list_spec.js
index 57fbb0bd917..5392ab71816 100644
--- a/spec/frontend/integrations/beyond_identity/components/exclusions_list_spec.js
+++ b/spec/frontend/integrations/beyond_identity/components/exclusions_list_spec.js
@@ -36,7 +36,7 @@ describe('ExclusionsList component', () => {
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findConfirmRemoveModal = () => wrapper.findComponent(ConfirmRemovalModal);
const findByText = (text) => wrapper.findByText(text);
- const findAddExclusionsButton = () => findByText('Add exclusions');
+ const findAddExclusionsButton = () => wrapper.findByTestId('add-exclusions-btn');
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findDrawer = () => wrapper.findComponent(AddExclusionsDrawer);
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index 9c4662ce38f..e2351583349 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -54,10 +54,10 @@ describe('Timeline events form', () => {
});
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
- const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save);
- const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd);
- const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel);
- const findDeleteButton = () => wrapper.findByText(timelineFormI18n.delete);
+ const findSubmitButton = () => wrapper.findByTestId('save-button');
+ const findSubmitAndAddButton = () => wrapper.findByTestId('save-and-add-button');
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findDeleteButton = () => wrapper.findByTestId('delete-button');
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
diff --git a/spec/frontend/notes/components/toggle_replies_widget_spec.js b/spec/frontend/notes/components/toggle_replies_widget_spec.js
index 5d47ae1237d..a0af0413ef2 100644
--- a/spec/frontend/notes/components/toggle_replies_widget_spec.js
+++ b/spec/frontend/notes/components/toggle_replies_widget_spec.js
@@ -18,10 +18,9 @@ describe('toggle replies widget for notes', () => {
const replies = [note, note, note, noteFromOtherUser, noteFromAnotherUser];
- const findCollapseToggleButton = () =>
- wrapper.findByRole('button', { text: ToggleRepliesWidget.i18n.collapseReplies });
- const findExpandToggleButton = () =>
- wrapper.findByRole('button', { text: ToggleRepliesWidget.i18n.expandReplies });
+ // const findCollapseToggleButton = () =>
+ // wrapper.findComponentByRole('button', { text: ToggleRepliesWidget.i18n.collapseReplies });
+ const findToggleButton = () => wrapper.findByTestId('replies-toggle');
const findRepliesButton = () => wrapper.findByRole('button', { text: '5 replies' });
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const findAvatars = () => wrapper.findComponent(GlAvatarsInline);
@@ -36,9 +35,8 @@ describe('toggle replies widget for notes', () => {
});
it('renders collapsed state elements', () => {
- expect(findExpandToggleButton().exists()).toBe(true);
- expect(findExpandToggleButton().props('icon')).toBe('chevron-right');
- expect(findExpandToggleButton().attributes('aria-label')).toBe('Expand replies');
+ expect(findToggleButton().props('icon')).toBe('chevron-right');
+ expect(findToggleButton().attributes('aria-label')).toBe('Expand replies');
expect(findAvatars().props('avatars')).toHaveLength(3);
expect(findRepliesButton().exists()).toBe(true);
expect(wrapper.text()).toContain('Last reply by');
@@ -47,7 +45,7 @@ describe('toggle replies widget for notes', () => {
});
it('emits "toggle" event when expand toggle button is clicked', () => {
- findExpandToggleButton().trigger('click');
+ findToggleButton().trigger('click');
expect(wrapper.emitted('toggle')).toEqual([[]]);
});
@@ -65,13 +63,12 @@ describe('toggle replies widget for notes', () => {
});
it('renders expanded state elements', () => {
- expect(findCollapseToggleButton().exists()).toBe(true);
- expect(findCollapseToggleButton().props('icon')).toBe('chevron-down');
- expect(findCollapseToggleButton().attributes('aria-label')).toBe('Collapse replies');
+ expect(findToggleButton().props('icon')).toBe('chevron-down');
+ expect(findToggleButton().attributes('aria-label')).toBe('Collapse replies');
});
it('emits "toggle" event when collapse toggle button is clicked', () => {
- findCollapseToggleButton().trigger('click');
+ findToggleButton().trigger('click');
expect(wrapper.emitted('toggle')).toEqual([[]]);
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rule_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rule_form_spec.js
index 1a5c5ce3809..420e8367ca6 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rule_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rule_form_spec.js
@@ -27,7 +27,7 @@ describe('container Protection Rule Form', () => {
wrapper.findByRole('textbox', { name: /repository path pattern/i });
const findMinimumAccessLevelForPushSelect = () =>
wrapper.findByRole('combobox', { name: /minimum access level for push/i });
- const findSubmitButton = () => wrapper.findByRole('button', { name: /add rule/i });
+ const findSubmitButton = () => wrapper.findByTestId('add-rule-btn');
const mountComponent = ({ config, provide = defaultProvidedValues } = {}) => {
wrapper = mountExtended(ContainerProtectionRuleForm, {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js
index 662f77b8c47..a041e8fb404 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js
@@ -280,15 +280,13 @@ describe('Container protection rules project settings', () => {
});
describe.each`
- comboboxName | minimumAccessLevelAttribute
- ${'Minimum access level for push'} | ${'minimumAccessLevelForPush'}
+ comboboxName | minimumAccessLevelAttribute
+ ${'push-access-select'} | ${'minimumAccessLevelForPush'}
`(
'column "$comboboxName" with selectbox (combobox)',
({ comboboxName, minimumAccessLevelAttribute }) => {
const findComboboxInTableRow = (i) =>
- extendedWrapper(findTableRow(i).findByRole('combobox', { name: comboboxName }));
- const findAllComboboxesInTableRow = (i) =>
- extendedWrapper(findTableRow(i).findAllByRole('combobox'));
+ extendedWrapper(wrapper.findAllByTestId(comboboxName).at(i));
it('contains correct access level as options', async () => {
createComponent();
@@ -358,25 +356,19 @@ describe('Container protection rules project settings', () => {
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
- findAllComboboxesInTableRow(0).wrappers.forEach((combobox) =>
- expect(combobox.props('disabled')).toBe(true),
- );
- expect(findTableRowButtonDelete(0).props('disabled')).toBe(true);
- findAllComboboxesInTableRow(1).wrappers.forEach((combobox) =>
- expect(combobox.props('disabled')).toBe(false),
- );
- expect(findTableRowButtonDelete(1).props('disabled')).toBe(false);
+ expect(findComboboxInTableRow(0).props('disabled')).toBe(true);
+ expect(findTableRowButtonDelete(0).attributes('disabled')).toBe('disabled');
+
+ expect(findComboboxInTableRow(1).props('disabled')).toBe(false);
+ expect(findTableRowButtonDelete(1).attributes('disabled')).toBeUndefined();
await waitForPromises();
- findAllComboboxesInTableRow(0).wrappers.forEach((combobox) =>
- expect(combobox.props('disabled')).toBe(false),
- );
- expect(findTableRowButtonDelete(0).props('disabled')).toBe(false);
- findAllComboboxesInTableRow(1).wrappers.forEach((combobox) =>
- expect(combobox.props('disabled')).toBe(false),
- );
- expect(findTableRowButtonDelete(1).props('disabled')).toBe(false);
+ expect(findComboboxInTableRow(0).props('disabled')).toBe(false);
+ expect(findTableRowButtonDelete(0).attributes('disabled')).toBeUndefined();
+
+ expect(findComboboxInTableRow(1).props('disabled')).toBe(false);
+ expect(findTableRowButtonDelete(1).attributes('disabled')).toBeUndefined();
});
it('handles erroneous graphql mutation', async () => {
@@ -485,9 +477,9 @@ describe('Container protection rules project settings', () => {
await clickOnModalPrimaryBtn();
- expect(findTableRowButtonDelete(0).props().disabled).toBe(true);
+ expect(findTableRowButtonDelete(0).attributes('disabled')).toBe('disabled');
- expect(findTableRowButtonDelete(1).props().disabled).toBe(false);
+ expect(findTableRowButtonDelete(1).attributes('disabled')).toBeUndefined();
});
it('sends graphql mutation', async () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js
index 72611933d31..1fb2ab4487e 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js
@@ -31,7 +31,7 @@ describe('Packages Protection Rule Form', () => {
const findPackageTypeSelect = () => wrapper.findByRole('combobox', { name: /type/i });
const findMinimumAccessLevelForPushSelect = () =>
wrapper.findByRole('combobox', { name: /minimum access level for push/i });
- const findSubmitButton = () => wrapper.findByRole('button', { name: /add rule/i });
+ const findSubmitButton = () => wrapper.findByTestId('add-rule-btn');
const findForm = () => wrapper.findComponent(GlForm);
const mountComponent = ({ data, config, provide = defaultProvidedValues } = {}) => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
index 5ba2b3732ef..811e4f0fc8c 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
@@ -35,7 +35,8 @@ describe('Packages protection rules project settings', () => {
extendedWrapper(wrapper.findByRole('table', { name: /protected packages/i }));
const findTableBody = () => extendedWrapper(findTable().findAllByRole('rowgroup').at(1));
const findTableRow = (i) => extendedWrapper(findTableBody().findAllByRole('row').at(i));
- const findTableRowButtonDelete = (i) => findTableRow(i).findByRole('button', { name: /delete/i });
+ const findTableRowButtonDelete = (i) =>
+ extendedWrapper(wrapper.findAllByTestId('delete-rule-btn').at(i));
const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findProtectionRuleForm = () => wrapper.findComponent(PackagesProtectionRuleForm);
const findAddProtectionRuleButton = () =>
@@ -262,9 +263,7 @@ describe('Packages protection rules project settings', () => {
describe('column "Minimum access level for push" with selectbox (combobox)', () => {
const findComboboxInTableRow = (i) =>
- extendedWrapper(
- findTableRow(i).findByRole('combobox', { name: /minimum access level for push/i }),
- );
+ extendedWrapper(wrapper.findAllByTestId('push-access-select').at(i));
it('contains combobox with respective access level', async () => {
createComponent();
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/rule_drawer_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/rule_drawer_spec.js
index 54e1f970621..f06e09153ca 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/rule_drawer_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/rule_drawer_spec.js
@@ -19,7 +19,7 @@ describe('Edit Rule Drawer', () => {
let wrapper;
const findDrawer = () => wrapper.findComponent(GlDrawer);
- const findCancelButton = () => wrapper.findByText('Cancel');
+ const findCancelButton = () => wrapper.findByTestId('cancel-btn');
const findHeader = () => wrapper.find('h2');
const findSaveButton = () => wrapper.findByTestId('save-allowed-to-merge');
const findCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
diff --git a/spec/frontend/projects/your_work/components/app_spec.js b/spec/frontend/projects/your_work/components/app_spec.js
index d3203d8dfd7..985b3e6be3e 100644
--- a/spec/frontend/projects/your_work/components/app_spec.js
+++ b/spec/frontend/projects/your_work/components/app_spec.js
@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { GlBadge, GlTabs, GlFilteredSearchToken } from '@gitlab/ui';
import projectCountsGraphQlResponse from 'test_fixtures/graphql/projects/your_work/project_counts.query.graphql.json';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import YourWorkProjectsApp from '~/projects/your_work/components/app.vue';
import TabView from '~/projects/your_work/components/tab_view.vue';
import { createRouter } from '~/projects/your_work';
@@ -85,7 +85,8 @@ describe('YourWorkProjectsApp', () => {
const findActiveTab = () => wrapper.findByRole('tab', { selected: true });
const findTabByName = (name) =>
wrapper.findAllByRole('tab').wrappers.find((tab) => tab.text().includes(name));
- const getTabCount = (tabName) => findTabByName(tabName).findComponent(GlBadge).text();
+ const getTabCount = (tabName) =>
+ extendedWrapper(findTabByName(tabName)).findByTestId('tab-counter-badge').text();
const findFilteredSearchAndSort = () => wrapper.findComponent(FilteredSearchAndSort);
const findTabView = () => wrapper.findComponent(TabView);
diff --git a/spec/frontend/ref/components/ambiguous_ref_modal_spec.js b/spec/frontend/ref/components/ambiguous_ref_modal_spec.js
index bb3fd0fa1f0..668fa0ec70f 100644
--- a/spec/frontend/ref/components/ambiguous_ref_modal_spec.js
+++ b/spec/frontend/ref/components/ambiguous_ref_modal_spec.js
@@ -29,9 +29,8 @@ describe('AmbiguousRefModal component', () => {
beforeEach(() => createComponent());
const findModal = () => wrapper.findComponent(GlModal);
- const findByText = (text) => wrapper.findByText(text);
- const findViewTagButton = () => findByText('View tag');
- const findViewBranchButton = () => findByText('View branch');
+ const findViewTagButton = () => wrapper.findByTestId('view-tag-btn');
+ const findViewBranchButton = () => wrapper.findByTestId('view-branch-btn');
it('renders a GlModal component with the correct props', () => {
expect(showModalSpy).toHaveBeenCalled();
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 31907639f1c..780352cfe84 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -157,7 +157,7 @@ const triggerApprovalUpdated = () => eventHub.$emit('ApprovalUpdated');
const triggerEditCommitInput = () =>
wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
const triggerEditSquashInput = (text) =>
- wrapper.find('[data-testid="squash-commit-message"]').vm.$emit('input', text);
+ findCommitEditWithInputId('squash-message-edit').vm.$emit('input', text);
const triggerEditMergeInput = (text) =>
wrapper.find('[data-testid="merge-commit-message"]').vm.$emit('input', text);
const findMergeHelperText = () => wrapper.find('[data-testid="auto-merge-helper-text"]');
@@ -417,54 +417,35 @@ describe('ReadyToMerge', () => {
describe('with squashing', () => {
const NEW_SQUASH_MESSAGE = 'updated squash message';
- it('sends the user-updated squash message', async () => {
+ beforeEach(async () => {
createComponent({
mr: { shouldRemoveSourceBranch: false, enableSquashBeforeMerge: true },
});
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest.spyOn(service, 'merge').mockResolvedValue(response('merge_when_pipeline_succeeds'));
await triggerEditCommitInput();
await findCheckboxElement().vm.$emit('input', true);
+ });
+
+ it('sends the user-updated squash message', async () => {
await triggerEditSquashInput(NEW_SQUASH_MESSAGE);
+ await findMergeButton().vm.$emit('click');
- findMergeButton().vm.$emit('click');
-
- await waitForPromises();
-
- const params = service.merge.mock.calls[0][0];
-
- expect(params).toEqual(
+ expect(service.merge).toHaveBeenCalledWith(
expect.objectContaining({
- sha: '12345678',
- should_remove_source_branch: false,
- auto_merge_strategy: 'merge_when_pipeline_succeeds',
squash_commit_message: NEW_SQUASH_MESSAGE,
}),
);
});
it('does not send the squash message if the user has not updated it', async () => {
- createComponent({
- mr: { shouldRemoveSourceBranch: false, enableSquashBeforeMerge: true },
- });
+ await findMergeButton().vm.$emit('click');
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(service, 'merge').mockResolvedValue(response('merge_when_pipeline_succeeds'));
-
- await triggerEditCommitInput();
- await findCheckboxElement().vm.$emit('input', true);
-
- findMergeButton().vm.$emit('click');
-
- await waitForPromises();
-
- const params = service.merge.mock.calls[0][0];
-
- expect(params).toEqual(
+ expect(service.merge).toHaveBeenCalledTimes(1);
+ expect(service.merge).toHaveBeenCalledWith(
expect.not.objectContaining({
- squash_commit_message: expect.any(String),
+ squash_commit_message: expect.anything(),
}),
);
});
@@ -473,49 +454,31 @@ describe('ReadyToMerge', () => {
describe('without squashing', () => {
const NEW_COMMIT_MESSAGE = 'updated commit message';
- it('sends the user-updated commit message', async () => {
+ beforeEach(async () => {
createComponent({ mr: { shouldRemoveSourceBranch: false } });
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest.spyOn(service, 'merge').mockResolvedValue(response('merge_when_pipeline_succeeds'));
+ await triggerEditCommitInput(); // Note this is intentional: `commit_message` shouldn't send until they actually edit it, even if they check the box
+ });
- await triggerEditCommitInput();
+ it('sends the user-updated commit message', async () => {
await triggerEditMergeInput(NEW_COMMIT_MESSAGE);
+ await findMergeButton().vm.$emit('click');
- findMergeButton().vm.$emit('click');
-
- await waitForPromises();
-
- const params = service.merge.mock.calls[0][0];
-
- expect(params).toEqual(
+ expect(service.merge).toHaveBeenCalledWith(
expect.objectContaining({
- sha: '12345678',
- should_remove_source_branch: false,
- auto_merge_strategy: 'merge_when_pipeline_succeeds',
commit_message: NEW_COMMIT_MESSAGE,
- squash: false,
- skip_merge_train: false,
}),
);
});
it('does not send the commit message if the user has not updated it', async () => {
- createComponent({ mr: { shouldRemoveSourceBranch: false } });
+ await findMergeButton().vm.$emit('click');
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(service, 'merge').mockResolvedValue(response('merge_when_pipeline_succeeds'));
-
- await triggerEditCommitInput(); // Note this is intentional: `commit_message` shouldn't send until they actually edit it, even if they check the box
- findMergeButton().vm.$emit('click');
-
- await waitForPromises();
-
- const params = service.merge.mock.calls[0][0];
-
- expect(params).toEqual(
+ expect(service.merge).toHaveBeenCalledTimes(1);
+ expect(service.merge).toHaveBeenCalledWith(
expect.not.objectContaining({
- commit_message: expect.any(String),
+ commit_message: expect.anything(),
}),
);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index 921cf341b19..e65603416d7 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -423,7 +423,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('when full report is clicked it should call the respective telemetry event', async () => {
expect(wrapper.vm.telemetryHub.fullReportClicked).not.toHaveBeenCalled();
- wrapper.findByText('Full report').vm.$emit('click');
+
+ wrapper.findByTestId('extension-actions-button').vm.$emit('click');
await nextTick();
expect(wrapper.vm.telemetryHub.fullReportClicked).toHaveBeenCalledTimes(1);
});
diff --git a/spec/frontend/vue_shared/components/list_selector/group_item_spec.js b/spec/frontend/vue_shared/components/list_selector/group_item_spec.js
index 2b7d2f5ba07..d840d6fa62a 100644
--- a/spec/frontend/vue_shared/components/list_selector/group_item_spec.js
+++ b/spec/frontend/vue_shared/components/list_selector/group_item_spec.js
@@ -18,7 +18,7 @@ describe('GroupItem spec', () => {
};
const findAvatar = () => wrapper.findComponent(GlAvatar);
- const findDeleteButton = () => wrapper.findByRole('button', { fullName: 'Delete Group 1' });
+ const findDeleteButton = () => wrapper.findByTestId('delete-group-btn');
beforeEach(() => createComponent());
diff --git a/spec/frontend/vue_shared/components/list_selector/user_item_spec.js b/spec/frontend/vue_shared/components/list_selector/user_item_spec.js
index 0a95f77a9db..9239984492b 100644
--- a/spec/frontend/vue_shared/components/list_selector/user_item_spec.js
+++ b/spec/frontend/vue_shared/components/list_selector/user_item_spec.js
@@ -17,7 +17,7 @@ describe('UserItem spec', () => {
};
const findAvatar = () => wrapper.findComponent(GlAvatar);
- const findDeleteButton = () => wrapper.findByRole('button', { name: 'Delete Admin' });
+ const findDeleteButton = () => wrapper.findByTestId('delete-user-btn');
beforeEach(() => createComponent());
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index be283ebfc63..32ac6b758d9 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -321,7 +321,7 @@ describe('Markdown field header component', () => {
await nextTick();
};
- const findFindInput = () => wrapper.findByPlaceholderText('Find');
+ const findFindInput = () => wrapper.findByTestId('find-btn');
beforeEach(() => {
createWrapper({ attachTo: createParentForm() });
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
index 3061b1a2a17..c76b7e37c1f 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -68,10 +68,9 @@ describe('ProjectsListItem', () => {
};
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
- const findMergeRequestsLink = () =>
- wrapper.findByRole('link', { name: ProjectsListItem.i18n.mergeRequests });
- const findIssuesLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.issues });
- const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks });
+ const findMergeRequestsLink = () => wrapper.findByTestId('mrs-btn');
+ const findIssuesLink = () => wrapper.findByTestId('issues-btn');
+ const findForksLink = () => wrapper.findByTestId('forks-btn');
const findProjectTopics = () => wrapper.findByTestId('project-topics');
const findPopover = () => findProjectTopics().findComponent(GlPopover);
const findProjectDescription = () => wrapper.findByTestId('project-description');
@@ -175,7 +174,7 @@ describe('ProjectsListItem', () => {
it('renders stars count', () => {
createComponent();
- const starsLink = wrapper.findByRole('link', { name: ProjectsListItem.i18n.stars });
+ const starsLink = wrapper.findByTestId('stars-btn');
const tooltip = getBinding(starsLink.element, 'gl-tooltip');
expect(tooltip.value).toBe(ProjectsListItem.i18n.stars);
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
index 2a2d09d0fac..0ac4d831d0e 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
@@ -40,7 +40,7 @@ describe('RunnerAwsInstructions', () => {
.findAllComponents(GlButton)
.filter((w) => w.props('variant') === 'confirm')
.at(0);
- const findCloseButton = () => wrapper.findByText('Close');
+ const findCloseButton = () => wrapper.findByTestId('close-btn');
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(RunnerAwsInstructions, {
diff --git a/spec/frontend/work_items/components/work_item_drawer_spec.js b/spec/frontend/work_items/components/work_item_drawer_spec.js
index e45489ed81a..ec8489304e3 100644
--- a/spec/frontend/work_items/components/work_item_drawer_spec.js
+++ b/spec/frontend/work_items/components/work_item_drawer_spec.js
@@ -284,8 +284,9 @@ describe('WorkItemDrawer', () => {
expect(visitUrl).toHaveBeenCalledWith('test');
});
- it('calls `router.push` when link is a work item path', () => {
+ it('calls `router.push` when link is a group level work item and we are at the group level', () => {
createComponent({
+ isGroup: true,
activeItem: {
iid: '1',
webUrl: '/groups/gitlab-org/gitlab/-/work_items/1',
@@ -298,14 +299,54 @@ describe('WorkItemDrawer', () => {
expect(mockRouterPush).toHaveBeenCalledWith({ name: 'workItem', params: { iid: '1' } });
});
- it('calls `router.push` when issue as work item view is enabled', () => {
- createComponent({ isGroup: false, workItemsViewPreference: true });
+ it('does not call `router.push` when link is a group level work item and we are at the project level', () => {
+ createComponent({
+ isGroup: false,
+ activeItem: {
+ iid: '1',
+ webUrl: '/groups/gitlab-org/gitlab/-/work_items/1',
+ fullPath: 'gitlab-org/gitlab',
+ },
+ });
+ findLinkButton().vm.$emit('click', new MouseEvent('click'));
+
+ expect(visitUrl).toHaveBeenCalledWith('/groups/gitlab-org/gitlab/-/work_items/1');
+ expect(mockRouterPush).not.toHaveBeenCalled();
+ });
+
+ it('calls `router.push` when issue as work item view is enabled and work item is in same project', () => {
+ createComponent({
+ isGroup: false,
+ workItemsViewPreference: true,
+ activeItem: {
+ iid: '1',
+ webUrl: '/gitlab-org/gitlab/-/work_items/1',
+ fullPath: 'gitlab-org/gitlab',
+ },
+ });
findLinkButton().vm.$emit('click', new MouseEvent('click'));
expect(visitUrl).not.toHaveBeenCalled();
expect(mockRouterPush).toHaveBeenCalledWith({ name: 'workItem', params: { iid: '1' } });
});
+
+ it('does not call `router.push` when issue as work item view is enabled and work item is in different project', () => {
+ createComponent({
+ isGroup: false,
+ workItemsViewPreference: true,
+ activeItem: {
+ iid: '1',
+ webUrl: '/gitlab-org/gitlab-other/-/work_items/1',
+ fullPath: 'gitlab-org/gitlab',
+ },
+ });
+
+ findLinkButton().vm.$emit('click', new MouseEvent('click'));
+
+ expect(visitUrl).toHaveBeenCalledWith('/gitlab-org/gitlab-other/-/work_items/1');
+ expect(mockRouterPush).not.toHaveBeenCalled();
+ });
});
describe('when `activeItem` prop is changed and it contains an `id`', () => {
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
index 229a0dea0c3..076d0982a86 100644
--- a/spec/frontend/work_items/utils_spec.js
+++ b/spec/frontend/work_items/utils_spec.js
@@ -17,6 +17,7 @@ import {
makeDrawerUrlParam,
makeDrawerItemFullPath,
getItems,
+ canRouterNav,
} from '~/work_items/utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { TYPE_EPIC } from '~/issues/constants';
@@ -303,3 +304,33 @@ describe('`getItems`', () => {
expect(result).toEqual(openChildren);
});
});
+
+describe('canRouterNav', () => {
+ const projectFullPath = 'gitlab-org/gitlab';
+ const groupFullPath = 'gitlab-org';
+ const projectWebUrl = (fullPath = projectFullPath) => `/${fullPath}/-/issues/1`;
+ const groupWebUrl = (fullPath = groupFullPath) => `/groups/${fullPath}/-/epics/1`;
+ it.each`
+ contextFullPath | targetWebUrl | contextIsGroup | issueAsWorkItem | shouldRouterNav
+ ${projectFullPath} | ${projectWebUrl()} | ${false} | ${false} | ${false}
+ ${projectFullPath} | ${projectWebUrl()} | ${false} | ${true} | ${true}
+ ${projectFullPath} | ${projectWebUrl('gitlab-org/gitlab-other')} | ${false} | ${false} | ${false}
+ ${projectFullPath} | ${projectWebUrl('gitlab-org/gitlab-other')} | ${false} | ${true} | ${false}
+ ${groupFullPath} | ${groupWebUrl()} | ${true} | ${false} | ${true}
+ ${groupFullPath} | ${groupWebUrl()} | ${true} | ${true} | ${true}
+ ${groupFullPath} | ${groupWebUrl('gitlab-other')} | ${true} | ${false} | ${false}
+ ${groupFullPath} | ${groupWebUrl('gitlab-other')} | ${true} | ${true} | ${false}
+ `(
+ `returns $shouldRouterNav when fullPath is $contextFullPath, webUrl is $targetWebUrl, isGroup is $contextIsGroup, and issueAsWorkItem is $issueAsWorkItem`,
+ ({ contextFullPath, targetWebUrl, contextIsGroup, issueAsWorkItem, shouldRouterNav }) => {
+ expect(
+ canRouterNav({
+ fullPath: contextFullPath,
+ webUrl: targetWebUrl,
+ isGroup: contextIsGroup,
+ issueAsWorkItem,
+ }),
+ ).toBe(shouldRouterNav);
+ },
+ );
+});
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index 5723d1a4b7a..6624f3b14b5 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -174,4 +174,77 @@ RSpec.describe GitlabSchema.types['MergeRequest'], feature_category: :code_revie
end
end
end
+
+ shared_examples_for 'avoids N+1 queries' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:second_user) { create(:user) }
+
+ specify do
+ # Warm up table schema and other data
+ GitlabSchema.execute(query, context: { current_user: user })
+
+ control = ActiveRecord::QueryRecorder.new { GitlabSchema.execute(query, context: { current_user: user }) }
+
+ create_additional_resources
+
+ # Warm up table schema and other data
+ GitlabSchema.execute(query, context: { current_user: second_user })
+
+ expect { GitlabSchema.execute(query, context: { current_user: second_user }) }.not_to exceed_query_limit(control)
+ end
+ end
+
+ describe '#head_pipeline' do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, :with_head_pipeline, source_project: project) }
+
+ it_behaves_like 'avoids N+1 queries' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ mergeRequests {
+ nodes {
+ headPipeline {
+ id
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ def create_additional_resources
+ create(:merge_request, :with_head_pipeline, source_project: project, source_branch: 'improve/awesome')
+ create(:merge_request, :with_head_pipeline, source_project: project, source_branch: 'spooky-stuff')
+ end
+ end
+ end
+
+ describe '#source_branch_exists' do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
+ it_behaves_like 'avoids N+1 queries' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ mergeRequests {
+ nodes {
+ sourceBranchExists
+ }
+ }
+ }
+ }
+ )
+ end
+
+ def create_additional_resources
+ create(:merge_request, source_project: project, source_branch: 'improve/awesome')
+ create(:merge_request, source_project: project, source_branch: 'spooky-stuff')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners_spec.rb b/spec/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners_spec.rb
index 5abba43c7a7..8b4fb923b45 100644
--- a/spec/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners_spec.rb
@@ -8,12 +8,6 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillShardingKeyIdOnCiRunners, sc
let(:connection) { Ci::ApplicationRecord.connection }
- before do
- if table_oid('ci_runners_e59bb2812d').present?
- connection.execute("ALTER TABLE ci_runners_e59bb2812d DROP CONSTRAINT IF EXISTS check_sharding_key_id_nullness")
- end
- end
-
describe '#perform' do
let(:runners) { table(:ci_runners) }
let(:runner_projects) { table(:ci_runner_projects) }
diff --git a/spec/lib/gitlab/background_migration/delete_orphaned_partitioned_ci_runner_machine_records_spec.rb b/spec/lib/gitlab/background_migration/delete_orphaned_partitioned_ci_runner_machine_records_spec.rb
new file mode 100644
index 00000000000..7bfe249c10c
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_orphaned_partitioned_ci_runner_machine_records_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedPartitionedCiRunnerMachineRecords,
+ schema: 20241112165507, feature_category: :fleet_visibility, migration: :gitlab_ci do
+ let(:connection) { Ci::ApplicationRecord.connection }
+ let(:runners) { table(:ci_runners_e59bb2812d, database: :ci, primary_key: :id) }
+ let(:runner_managers) { table(:ci_runner_machines_687967fa8a, database: :ci, primary_key: :id) }
+ let(:orphaned_group_runner_manager) { runner_managers.find(3) }
+ let(:orphaned_project_runner_manager) { runner_managers.find(5) }
+
+ around do |example|
+ # Allow creating legacy runner managers (created when FK was not present) that are not connected to a runner
+ connection.transaction do
+ connection.execute(<<~SQL)
+ ALTER TABLE group_type_ci_runner_machines_687967fa8a DISABLE TRIGGER ALL;
+ ALTER TABLE project_type_ci_runner_machines_687967fa8a DISABLE TRIGGER ALL;
+ SQL
+
+ example.run
+
+ connection.execute(<<~SQL)
+ ALTER TABLE group_type_ci_runner_machines_687967fa8a ENABLE TRIGGER ALL;
+ ALTER TABLE project_type_ci_runner_machines_687967fa8a ENABLE TRIGGER ALL;
+ SQL
+ end
+ end
+
+ before do
+ create_runner_and_runner_manager(id: 1, runner_type: 1, system_xid: 'system1')
+ create_runner_and_runner_manager(id: 2, runner_type: 2, sharding_key_id: 89, system_xid: 'system2')
+ runner_managers.create!(
+ id: 3, runner_id: non_existing_record_id, runner_type: 2, sharding_key_id: 100, system_xid: 'system2'
+ )
+ create_runner_and_runner_manager(id: 4, runner_type: 3, sharding_key_id: 10, system_xid: 'system2')
+ runner_managers.create!(
+ id: 5, runner_id: non_existing_record_id, runner_type: 3, sharding_key_id: 100, system_xid: 'system3'
+ )
+ end
+
+ describe '#perform' do
+ subject(:migration) do
+ described_class.new(
+ start_id: runner_managers.minimum(:runner_id),
+ end_id: runner_managers.maximum(:runner_id),
+ batch_table: :ci_runner_machines_687967fa8a,
+ batch_column: :runner_id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: connection
+ )
+ end
+
+ it 'deletes from ci_runner_machines_687967fa8a where runner_id has no related', :aggregate_failures do
+ instance_runner_manager = runner_managers.where(runner_type: 1).first
+
+ expect { migration.perform }.to change { runner_managers.count }.from(5).to(3)
+
+ expect(instance_runner_manager.reload).to be_persisted
+ expect { orphaned_group_runner_manager.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { orphaned_project_runner_manager.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ private
+
+ def create_runner_and_runner_manager(**attrs)
+ runner = runners.create!(**attrs.except(:system_xid))
+ runner_managers.create!(runner_id: runner.id, **attrs)
+ end
+end
diff --git a/spec/migrations/20241112165507_queue_delete_orphaned_p_ci_runner_machine_records_on_dot_com_spec.rb b/spec/migrations/20241112165507_queue_delete_orphaned_p_ci_runner_machine_records_on_dot_com_spec.rb
new file mode 100644
index 00000000000..d2dd7645d14
--- /dev/null
+++ b/spec/migrations/20241112165507_queue_delete_orphaned_p_ci_runner_machine_records_on_dot_com_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteOrphanedPCiRunnerMachineRecordsOnDotCom, migration: :gitlab_ci, feature_category: :fleet_visibility do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ before do
+ allow(Gitlab).to receive(:com_except_jh?).and_return(gitlab_com_except_jh?)
+ end
+
+ context 'when it is not on GitLab.com' do
+ let(:gitlab_com_except_jh?) { false }
+
+ it 'does not schedule a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+ end
+ end
+ end
+
+ context 'when it is on GitLab.com' do
+ let(:gitlab_com_except_jh?) { true }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :ci_runner_machines_687967fa8a,
+ column_name: :runner_id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE,
+ gitlab_schema: :gitlab_ci
+ )
+ }
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 3946d6b278b..684e4895d04 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1402,10 +1402,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/fonts/-/fonts-1.3.0.tgz#df89c1bb6714e4a8a5d3272568aa4de7fb337267"
integrity sha512-DoMUIN3DqjEn7wvcxBg/b7Ite5fTdF5EmuOZoBRo2j0UBGweDXmNBi+9HrTZs4cBU660dOxcf1hATFcG3npbPg==
-"@gitlab/query-language@^0.0.5-a-20241105":
- version "0.0.5-a-20241105"
- resolved "https://registry.yarnpkg.com/@gitlab/query-language/-/query-language-0.0.5-a-20241105.tgz#6ad77d3278290d16e5fa5c9111f9c87145c1379a"
- integrity sha512-PCmbdKTnnMpW3GTcIz++YappqPw5xMfcKukeSGgkrwQglIAXtP0tMW+loDe4wKjBeieJTpuuR+YwuNVAwN6rew==
+"@gitlab/query-language@^0.0.5-a-20241112":
+ version "0.0.5-a-20241112"
+ resolved "https://registry.yarnpkg.com/@gitlab/query-language/-/query-language-0.0.5-a-20241112.tgz#f442477e2728169fba4740a74d10cb887f9f0067"
+ integrity sha512-NZ+k2/DyR5XDW2vqISTg0WVxz3EQhDJzwq2btbPY5ZKcd4gsD9SIN4Xqquw/L5IfRia16wdy08YsdUImr81e7g==
dependencies:
"@bjorn3/browser_wasi_shim" "^0.2.19"