+
+
-
+
{{ fullName }}
@{{ name }}
@@ -68,5 +68,5 @@ export default {
category="tertiary"
@click="$emit('delete', data.id)"
/>
-
+
diff --git a/db/docs/batched_background_migrations/create_compliance_standards_adherence.yml b/db/docs/batched_background_migrations/create_compliance_standards_adherence.yml
index 0f0d47b39bc..8970593d42f 100644
--- a/db/docs/batched_background_migrations/create_compliance_standards_adherence.yml
+++ b/db/docs/batched_background_migrations/create_compliance_standards_adherence.yml
@@ -1,7 +1,9 @@
---
migration_job_name: CreateComplianceStandardsAdherence
-description: This migration creates 'project_compliance_standards_adherence' table for existing projects
+description: This migration creates 'project_compliance_standards_adherence' table
+ for existing projects
feature_category: compliance_management
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129941
queued_migration_version: 20230818142801
milestone: '16.4'
+finalized_by: '20240707231815'
diff --git a/db/docs/geo_repositories_changed_events.yml b/db/docs/deleted_tables/geo_repositories_changed_events.yml
similarity index 77%
rename from db/docs/geo_repositories_changed_events.yml
rename to db/docs/deleted_tables/geo_repositories_changed_events.yml
index 738779586c4..c15a42fd27e 100644
--- a/db/docs/geo_repositories_changed_events.yml
+++ b/db/docs/deleted_tables/geo_repositories_changed_events.yml
@@ -8,4 +8,5 @@ description: Geo event for when the repositories for selective sync of a specifi
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/312bc703a4619b87ba2ac4e59623e7747a24502c
milestone: '9.5'
gitlab_schema: gitlab_main_cell
-sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/464364 # this table will be deleted soon
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158660
+removed_in_milestone: '17.2'
diff --git a/db/post_migrate/20240707231815_finalize_create_compliance_standards_adherence.rb b/db/post_migrate/20240707231815_finalize_create_compliance_standards_adherence.rb
new file mode 100644
index 00000000000..aa85209e3c3
--- /dev/null
+++ b/db/post_migrate/20240707231815_finalize_create_compliance_standards_adherence.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class FinalizeCreateComplianceStandardsAdherence < Gitlab::Database::Migration[2.2]
+ milestone '17.2'
+
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ ensure_batched_background_migration_is_finished(
+ job_class_name: 'CreateComplianceStandardsAdherence',
+ table_name: :projects,
+ column_name: :id,
+ job_arguments: [],
+ finalize: true
+ )
+ end
+
+ def down; end
+end
diff --git a/db/post_migrate/20240708175722_remove_foreign_key_geo_event_log.rb b/db/post_migrate/20240708175722_remove_foreign_key_geo_event_log.rb
new file mode 100644
index 00000000000..21cde24181d
--- /dev/null
+++ b/db/post_migrate/20240708175722_remove_foreign_key_geo_event_log.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class RemoveForeignKeyGeoEventLog < Gitlab::Database::Migration[2.2]
+ milestone '17.2'
+
+ disable_ddl_transaction!
+
+ FROM_TABLE = :geo_event_log
+ TO_TABLE = :geo_repositories_changed_events
+
+ def up
+ with_lock_retries do
+ remove_foreign_key(
+ FROM_TABLE,
+ TO_TABLE,
+ column: :repositories_changed_event_id,
+ if_exists: true
+ )
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(
+ FROM_TABLE,
+ TO_TABLE,
+ name: :fk_4a99ebfd60,
+ column: :repositories_changed_event_id,
+ on_delete: :cascade
+ )
+ end
+end
diff --git a/db/post_migrate/20240708180142_drop_geo_repositories_changed_events.rb b/db/post_migrate/20240708180142_drop_geo_repositories_changed_events.rb
new file mode 100644
index 00000000000..49cc0d74c52
--- /dev/null
+++ b/db/post_migrate/20240708180142_drop_geo_repositories_changed_events.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class DropGeoRepositoriesChangedEvents < Gitlab::Database::Migration[2.2]
+ milestone '17.2'
+
+ disable_ddl_transaction!
+
+ def up
+ drop_table :geo_repositories_changed_events
+ end
+
+ def down
+ create_table :geo_repositories_changed_events do |t|
+ t.integer :geo_node_id,
+ index: { name: 'index_geo_repositories_changed_events_on_geo_node_id' },
+ null: false
+ end
+
+ add_concurrent_foreign_key(
+ :geo_repositories_changed_events,
+ :geo_nodes,
+ name: :fk_rails_75ec0fefcc,
+ column: :geo_node_id,
+ on_delete: :cascade
+ )
+ end
+end
diff --git a/db/schema_migrations/20240707231815 b/db/schema_migrations/20240707231815
new file mode 100644
index 00000000000..1fecad6119a
--- /dev/null
+++ b/db/schema_migrations/20240707231815
@@ -0,0 +1 @@
+274a6acea5c5940bd76616e7bd9c6e7c1d58f1ca133f56c00b930bdf094d85a3
\ No newline at end of file
diff --git a/db/schema_migrations/20240708175722 b/db/schema_migrations/20240708175722
new file mode 100644
index 00000000000..5c1ae672c94
--- /dev/null
+++ b/db/schema_migrations/20240708175722
@@ -0,0 +1 @@
+a500e7c16e274cd083b865dea60cafe1bd9618f8f5767f492947b7b43d7e9284
\ No newline at end of file
diff --git a/db/schema_migrations/20240708180142 b/db/schema_migrations/20240708180142
new file mode 100644
index 00000000000..a961e7f9f72
--- /dev/null
+++ b/db/schema_migrations/20240708180142
@@ -0,0 +1 @@
+23a8c778820f8a25cdbe7c864d3fa9b667a11d6b61e3d9e6ad2f5e7b32da93b9
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 207d55179eb..56e5accef61 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10695,20 +10695,6 @@ CREATE SEQUENCE geo_nodes_id_seq
ALTER SEQUENCE geo_nodes_id_seq OWNED BY geo_nodes.id;
-CREATE TABLE geo_repositories_changed_events (
- id bigint NOT NULL,
- geo_node_id integer NOT NULL
-);
-
-CREATE SEQUENCE geo_repositories_changed_events_id_seq
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-ALTER SEQUENCE geo_repositories_changed_events_id_seq OWNED BY geo_repositories_changed_events.id;
-
CREATE TABLE ghost_user_migrations (
id bigint NOT NULL,
user_id bigint NOT NULL,
@@ -20864,8 +20850,6 @@ ALTER TABLE ONLY geo_node_statuses ALTER COLUMN id SET DEFAULT nextval('geo_node
ALTER TABLE ONLY geo_nodes ALTER COLUMN id SET DEFAULT nextval('geo_nodes_id_seq'::regclass);
-ALTER TABLE ONLY geo_repositories_changed_events ALTER COLUMN id SET DEFAULT nextval('geo_repositories_changed_events_id_seq'::regclass);
-
ALTER TABLE ONLY ghost_user_migrations ALTER COLUMN id SET DEFAULT nextval('ghost_user_migrations_id_seq'::regclass);
ALTER TABLE ONLY gitlab_subscription_histories ALTER COLUMN id SET DEFAULT nextval('gitlab_subscription_histories_id_seq'::regclass);
@@ -23018,9 +23002,6 @@ ALTER TABLE ONLY geo_node_statuses
ALTER TABLE ONLY geo_nodes
ADD CONSTRAINT geo_nodes_pkey PRIMARY KEY (id);
-ALTER TABLE ONLY geo_repositories_changed_events
- ADD CONSTRAINT geo_repositories_changed_events_pkey PRIMARY KEY (id);
-
ALTER TABLE ONLY ghost_user_migrations
ADD CONSTRAINT ghost_user_migrations_pkey PRIMARY KEY (id);
@@ -27289,8 +27270,6 @@ CREATE UNIQUE INDEX index_geo_nodes_on_name ON geo_nodes USING btree (name);
CREATE INDEX index_geo_nodes_on_primary ON geo_nodes USING btree ("primary");
-CREATE INDEX index_geo_repositories_changed_events_on_geo_node_id ON geo_repositories_changed_events USING btree (geo_node_id);
-
CREATE INDEX index_ghost_user_migrations_on_consume_after_id ON ghost_user_migrations USING btree (consume_after, id);
CREATE UNIQUE INDEX index_ghost_user_migrations_on_user_id ON ghost_user_migrations USING btree (user_id);
@@ -32254,9 +32233,6 @@ ALTER TABLE ONLY releases
ALTER TABLE ONLY workspace_variables
ADD CONSTRAINT fk_494e093520 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
-ALTER TABLE ONLY geo_event_log
- ADD CONSTRAINT fk_4a99ebfd60 FOREIGN KEY (repositories_changed_event_id) REFERENCES geo_repositories_changed_events(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY user_namespace_callouts
ADD CONSTRAINT fk_4b1257f385 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -34237,9 +34213,6 @@ ALTER TABLE ONLY release_links
ALTER TABLE ONLY milestone_releases
ADD CONSTRAINT fk_rails_754f27dbfa FOREIGN KEY (release_id) REFERENCES releases(id) ON DELETE CASCADE;
-ALTER TABLE ONLY geo_repositories_changed_events
- ADD CONSTRAINT fk_rails_75ec0fefcc FOREIGN KEY (geo_node_id) REFERENCES geo_nodes(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY resource_label_events
ADD CONSTRAINT fk_rails_75efb0a653 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE CASCADE;
diff --git a/doc/administration/packages/container_registry_metadata_database.md b/doc/administration/packages/container_registry_metadata_database.md
index c2e630876c5..9fb22e99f22 100644
--- a/doc/administration/packages/container_registry_metadata_database.md
+++ b/doc/administration/packages/container_registry_metadata_database.md
@@ -551,3 +551,51 @@ If either of these values is set to `on`, you must disable it:
1. Restart your Postgres server to apply these settings.
1. Try to [apply schema migrations](#apply-schema-migrations) again, if applicable.
1. Restart the registry `sudo gitlab-ctl restart registry`.
+
+### Error: `cannot import all repositories while the tags table has entries`
+
+If you try to [migrate existing registries](#existing-registries) and encounter the following error:
+
+```shell
+ERRO[0000] cannot import all repositories while the tags table has entries, you must truncate the table manually before retrying,
+see https://docs.gitlab.com/ee/administration/packages/container_registry_metadata_database.html#troubleshooting
+common_blobs=true dry_run=false error="tags table is not empty"
+```
+
+This error happens when there are existing entries in the `tags` table of the registry database,
+which can happen if you:
+
+- Attempted the [one step migration](#one-step-migration) and encountered errors.
+- Attempted the [three-step migration](#three-step-migration) process and encountered errors.
+- Stopped the migration process on purpose.
+- Tried to run the migration again after any of the above.
+- Ran the migration against the wrong configuration file.
+
+To resolve this issue, you must delete the existing entries in the tags table.
+You must truncate the table manually on your PostgreSQL instance:
+
+1. Edit `/etc/gitlab/gitlab.rb` and ensure the metadata database is **disabled**:
+
+ ```ruby
+ registry['database'] = {
+ 'enabled' => false,
+ 'host' => 'localhost',
+ 'port' => 5432,
+ 'user' => 'registry-database-user',
+ 'password' => 'registry-database-password',
+ 'dbname' => 'registry-database-name',
+ 'sslmode' => 'require', # See the PostgreSQL documentation for additional information https://www.postgresql.org/docs/current/libpq-ssl.html.
+ 'sslcert' => '/path/to/cert.pem',
+ 'sslkey' => '/path/to/private.key',
+ 'sslrootcert' => '/path/to/ca.pem'
+ }
+ ```
+
+1. Connect to your registry database using a PostgreSQL client.
+1. Truncate the `tags` table to remove all existing entries:
+
+ ```sql
+ TRUNCATE TABLE tags RESTART IDENTITY CASCADE;
+ ```
+
+1. After truncating the `tags` table, try running the migration process again.
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 97493ed2e96..31a72910446 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -769,6 +769,54 @@ beforeEach(() => {
});
```
+## Console warnings and errors in tests
+
+Unexpected console warnings and errors are indicative of problems in our production code.
+We want our test environment to be strict, so your tests should fail when unexpected
+`console.error` or `console.warn` calls are made.
+
+### Ignoring console messages from watcher
+
+Since there's a lot of code outside of our control, there are some console messages that
+are ignored by default and will **not** fail a test if used. This list of ignored
+messages can be maintained where we call `setupConsoleWatcher`. Example:
+
+```javascript
+setupConsoleWatcher({
+ ignores: [
+ ...,
+ // Any call to `console.error('Foo bar')` or `console.warn('Foo bar')` will be ignored by our console watcher.
+ 'Foo bar',
+ // Use regex to allow for flexible message matching.
+ /Lorem ipsum/,
+ ]
+});
+```
+
+If a specific test needs to ignore a specific message for a `describe` block, use the
+`ignoreConsoleMessages` helper near the top of the `describe`. This automatically
+calls `beforeAll` and `afterAll` to set up/teardown this set of ignored for the test
+context.
+
+Use this sparingly and only if absolutely necessary for test maintainability. Example:
+
+```javascript
+import { ignoreConsoleMessages } from 'helpers/console_watcher';
+
+describe('foos/components/foo.vue', () => {
+ describe('when blooped', () => {
+ // Will not fail a test if `console.warn('Lorem ipsum')` is called
+ ignoreConsoleMessages([
+ /^Lorem ipsum/
+ ]);
+ });
+
+ describe('default', () => {
+ // Will fail a test if `console.warn('Lorem ipsum')` is called
+ });
+});
+```
+
## Factories
TBU
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ea25ed9c85b..8d11d72078d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -46490,12 +46490,21 @@ msgstr ""
msgid "ScanExecutionPolicy|Add new condition"
msgstr ""
+msgid "ScanExecutionPolicy|Are you sure you want to create merge request fot this policy?"
+msgstr ""
+
+msgid "ScanExecutionPolicy|Back to edit policy"
+msgstr ""
+
msgid "ScanExecutionPolicy|Choose a method to execute code"
msgstr ""
msgid "ScanExecutionPolicy|Conditions"
msgstr ""
+msgid "ScanExecutionPolicy|Create merge request"
+msgstr ""
+
msgid "ScanExecutionPolicy|Create new scan profile"
msgstr ""
@@ -46547,6 +46556,9 @@ msgstr ""
msgid "ScanExecutionPolicy|Override"
msgstr ""
+msgid "ScanExecutionPolicy|Potential overload for infrastructure"
+msgstr ""
+
msgid "ScanExecutionPolicy|Run %{scan} with the following options:"
msgstr ""
@@ -46610,6 +46622,9 @@ msgstr ""
msgid "ScanExecutionPolicy|The file path can't be empty"
msgstr ""
+msgid "ScanExecutionPolicy|This scan execution policy will generate a large number of pipelines, which can have a significant performance impact. To reduce potential performance issues, consider creating separate policies for smaller subsets of projects."
+msgstr ""
+
msgid "ScanExecutionPolicy|Triggers:"
msgstr ""
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 3f8ce41ebeb..d3677c008a8 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe 'Database schema', feature_category: :database do
epics: %w[updated_by_id last_edited_by_id state_id],
events: %w[target_id],
forked_project_links: %w[forked_from_project_id],
- geo_event_log: %w[hashed_storage_attachments_event_id],
+ geo_event_log: %w[hashed_storage_attachments_event_id repositories_changed_event_id],
geo_node_statuses: %w[last_event_id cursor_last_event_id],
geo_nodes: %w[oauth_application_id],
geo_repository_deleted_events: %w[project_id],
diff --git a/spec/frontend/__helpers__/console_watcher.js b/spec/frontend/__helpers__/console_watcher.js
new file mode 100644
index 00000000000..d1105364e21
--- /dev/null
+++ b/spec/frontend/__helpers__/console_watcher.js
@@ -0,0 +1,244 @@
+const METHODS_TO_WATCH = ['error', 'warn'];
+
+const matchesStringOrRegex = (target, strOrRegex) => {
+ if (typeof strOrRegex === 'string') {
+ return target === strOrRegex;
+ }
+
+ // why: We can't just check `instanceof RegExp` for some reason. I think it happens when values cross the Jest test sandbox into the outer environment.
+ // Please see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145065#note_1788386920
+ if ('test' in strOrRegex) {
+ return strOrRegex.test(target);
+ }
+
+ throw new Error(`Unexpected value to match (${strOrRegex}). Expected string or RegExp.`);
+};
+
+export const throwErrorFromCalls = (consoleCalls) => {
+ const consoleCallsList = consoleCalls
+ .map(({ method, args }, idx) => `\n[${idx + 1}] ${method}: ${args}\n`)
+ .join('')
+ .split('\n')
+ .map((x) => `\t${x}`)
+ .join('\n');
+
+ throw new Error(
+ `Unexpected calls to console (${consoleCalls.length}) with:\n${consoleCallsList}\n`,
+ );
+};
+
+class ConsoleWatcher {
+ /**
+ * Reference to the console instance that we are watching and overriding.
+ *
+ * @type {Console}
+ */
+ #console;
+
+ /**
+ * Array of RegExp's or string's that will be used to ignore certain calls.
+ * These are applied to only the message received by the `ConsoleWatcher`, regardless
+ * of whether it was a `console.warn` or `console.error`.
+ *
+ * @type {(RegExp | string)[]}
+ */
+ #ignores;
+
+ /**
+ * List of console method calls that were collected. This can include ignored consoles.
+ * We don't filter out ignores until we `getCalls`.
+ *
+ * @type {{method: string, args: unknown[]}[]}
+ */
+ #calls;
+
+ /**
+ * @type {{ error: Function, warn: Function }} Reference to the original Console methods
+ */
+ #original;
+
+ /**
+ * Flag for whether to use the legacy behavior of throwing immediately
+ *
+ * @type {boolean}
+ */
+ #shouldThrowImmediately;
+
+ constructor(console, { ignores = [], shouldThrowImmediately = false } = {}) {
+ this.#console = console;
+ this.#ignores = ignores;
+ this.#calls = [];
+ this.#original = {};
+ this.#shouldThrowImmediately = shouldThrowImmediately;
+
+ METHODS_TO_WATCH.forEach((method) => {
+ const originalFn = console[method];
+
+ this.#original[method] = originalFn;
+
+ Object.assign(console, {
+ [method]: (...args) => {
+ this.#handleCall(method, args);
+ },
+ });
+ });
+ }
+
+ dispose() {
+ Object.entries(this.#original).forEach(([key, fn]) => {
+ Object.assign(this.#console, { [key]: fn });
+ });
+ }
+
+ getIgnores() {
+ return this.#ignores;
+ }
+
+ setIgnores(ignores) {
+ this.#ignores = ignores;
+ }
+
+ setShouldThrowImmediately(value) {
+ this.#shouldThrowImmediately = value;
+ }
+
+ shouldThrowImmediately() {
+ return this.#shouldThrowImmediately;
+ }
+
+ forgetCalls() {
+ this.#calls = [];
+ }
+
+ getCalls() {
+ return this.#calls.filter((call) => !this.#shouldIgnore(call));
+ }
+
+ #shouldIgnore({ args }) {
+ const argsAsStr = args.map(String).join();
+
+ return this.#ignores.some((ignoreMatcher) => matchesStringOrRegex(argsAsStr, ignoreMatcher));
+ }
+
+ #handleCall(method, args) {
+ const call = { method, args };
+
+ if (this.#shouldThrowImmediately && !this.#shouldIgnore(call)) {
+ throwErrorFromCalls([call]);
+ return;
+ }
+
+ this.#calls.push(call);
+
+ this.#original[method](...args);
+ }
+}
+
+/**
+ * @param {CustomEnvironment} environment - Jest environment to attach the globals to
+ * @param {Console} console - the instnace of Console to setup watchers.
+ * @param {Object} options - optional options to use when setting up the console watcher.
+ * @param {(RegExp | string)[]} options.ignores - list of console messages to ignore.
+ * @param {boolean} options.shouldThrowImmediately - flag for whether we do the legacy behavior of throwing immediately.
+ * @returns
+ */
+export const setupConsoleWatcher = (environment, console, options) => {
+ if (environment.global.jestConsoleWatcher) {
+ throw new Error('jestConsoleWatcher already exists');
+ }
+
+ const consoleWatcher = new ConsoleWatcher(console, options);
+
+ // eslint-disable-next-line no-param-reassign
+ environment.global.jestConsoleWatcher = consoleWatcher;
+
+ return consoleWatcher;
+};
+
+export const forgetConsoleCalls = () => global.jestConsoleWatcher?.forgetCalls();
+
+export const getConsoleCalls = () => global.jestConsoleWatcher?.getCalls() || [];
+
+/**
+ * Flags whether or the current `describe` should adopt the legacy test behavior of throwing immediately on `console.warn` or `console.error`
+ *
+ * Example:
+ *
+ * ```javascript
+ * describe('Foo', () => {
+ * useConsoleWatcherThrowsImmediately();
+ *
+ * describe('bar', () => {
+ * useConsoleWatcherThrowsImmediately(false);
+ *
+ * // These tests **will not** throw immediately if `console.warn` or `console.error` is called.
+ * });
+ *
+ * describe('zed', () => {
+ * // These tests **will** throw immediately if `console.warn` or `console.error` is called.
+ * })
+ * });
+ * ```
+ *
+ * @param {boolean} val - True if the consoleWatcher should throw immediately on a console method call
+ * @deprecated This only exists to support legacy tests that depend on this erroneous test behavior
+ */
+export const useConsoleWatcherThrowsImmediately = (val = true) => {
+ let origLegacy;
+
+ beforeAll(() => {
+ origLegacy = global.jestConsoleWatcher.shouldThrowImmediately();
+
+ global.jestConsoleWatcher.setShouldThrowImmediately(val);
+ });
+
+ afterAll(() => {
+ global.jestConsoleWatcher.setShouldThrowImmediately(origLegacy);
+ });
+};
+
+/**
+ * Sets up the console watcher to ignore the given messages for the current `describe` block.
+ *
+ * Example:
+ *
+ * ```javascript
+ * describe('Foo', () => {
+ * ignoreConsoleMessages([
+ * 'Hello world',
+ * /The field .* is not okay/,
+ * ]);
+ *
+ * it('works', () => {
+ * // Passes :)
+ * console.error('Hello world');
+ * console.warn('The field FOO is not okay');
+ *
+ * // Fail :(
+ * console.error('Hello world, strings are compared strictly.');
+ * });
+ * });
+ * ```
+ *
+ * @param {(string | RegExp)[]} ignores - Array of console messages to ignore for the current `describe` block.
+ */
+export const ignoreConsoleMessages = (ignores) => {
+ if (!Array.isArray(ignores)) {
+ throw new Error('Expected ignoreConsoleMessages to receive an Array of strings or RegExp');
+ }
+
+ let origIgnores;
+
+ beforeAll(() => {
+ origIgnores = global.jestConsoleWatcher.getIgnores();
+
+ global.jestConsoleWatcher.setIgnores(origIgnores.concat(ignores));
+ });
+
+ afterAll(() => {
+ global.jestConsoleWatcher.setIgnores(origIgnores);
+ });
+};
+
+export const ignoreVueConsoleWarnings = () =>
+ ignoreConsoleMessages([/^\[Vue warn\]: Missing required prop/, /^\[Vue warn\]: Invalid prop/]);
diff --git a/spec/frontend/__helpers__/console_watcher_spec.js b/spec/frontend/__helpers__/console_watcher_spec.js
new file mode 100644
index 00000000000..73ce11cc12d
--- /dev/null
+++ b/spec/frontend/__helpers__/console_watcher_spec.js
@@ -0,0 +1,175 @@
+import {
+ setupConsoleWatcher,
+ throwErrorFromCalls,
+ forgetConsoleCalls,
+ getConsoleCalls,
+ ignoreConsoleMessages,
+ // eslint-disable-next-line import/no-deprecated
+ useConsoleWatcherThrowsImmediately,
+} from './console_watcher';
+
+const TEST_IGNORED_MESSAGE = 'Full message to ignore.';
+const TEST_IGNORED_REGEX_MESSAGE = 'Part of this message matches partial ignore.';
+
+describe('__helpers__/console_watcher', () => {
+ let testEnvironment;
+ let testConsole;
+ let testConsoleOriginalFn;
+ let consoleWatcher;
+
+ const callConsoleMethods = () => {
+ testConsole.log('Hello world log');
+ testConsole.info('Hello world info');
+ testConsole.info(TEST_IGNORED_MESSAGE);
+ testConsole.warn(TEST_IGNORED_REGEX_MESSAGE);
+ testConsole.warn('Hello world warn');
+ testConsole.error('Hello world error');
+ testConsole.error(TEST_IGNORED_MESSAGE);
+ };
+
+ // note: To test the beforeAll/afterAll behavior in some parts of console_watcher, we need to have our setup
+ // use beforeAll/afterAll instead of beforeEach/afterEach.
+ beforeAll(() => {
+ testEnvironment = { global: {} };
+ testConsole = {
+ log: (...args) => testConsoleOriginalFn('log', ...args),
+ info: (...args) => testConsoleOriginalFn('info', ...args),
+ warn: (...args) => testConsoleOriginalFn('warn', ...args),
+ error: (...args) => testConsoleOriginalFn('error', ...args),
+ };
+ Object.defineProperty(global, 'jestConsoleWatcher', {
+ get() {
+ return testEnvironment.global.jestConsoleWatcher;
+ },
+ });
+ });
+
+ beforeEach(() => {
+ // why: Let's make sure we have a fresh spy for every test
+ testConsoleOriginalFn = jest.fn();
+ });
+
+ afterEach(() => {
+ // why: We need to forget calls or else our main test_setup will pick up on console calls and throw an error
+ forgetConsoleCalls();
+ });
+
+ describe('throwErrorFromCalls', () => {
+ it('throws error with message containing calls', () => {
+ const calls = [
+ { method: 'error', args: ['Hello world', 2, 'Lorem\nIpsum\nDolar\nSit'] },
+ { method: 'info', args: [] },
+ { method: 'warn', args: ['Hello world', 'something bad happened'] },
+ ];
+
+ expect(() => throwErrorFromCalls(calls)).toThrowErrorMatchingInlineSnapshot(`
+"Unexpected calls to console (3) with:
+
+ [1] error: Hello world,2,Lorem
+ Ipsum
+ Dolar
+ Sit
+
+ [2] info:
+
+ [3] warn: Hello world,something bad happened
+
+"
+`);
+ });
+ });
+
+ describe('setupConsoleWatcher', () => {
+ beforeAll(() => {
+ testEnvironment = { global: {} };
+ consoleWatcher = setupConsoleWatcher(testEnvironment, testConsole, {
+ ignores: ['Full message to ignore.', /partial ignore/],
+ });
+ });
+
+ afterAll(() => {
+ consoleWatcher.dispose();
+ });
+
+ describe.each(['warn', 'error'])('with %s', (method) => {
+ it('with unexpected message, calls original console method', () => {
+ testConsole[method]('BOOM!');
+
+ expect(testConsoleOriginalFn).toHaveBeenCalledTimes(1);
+ expect(testConsoleOriginalFn).toHaveBeenCalledWith(method, 'BOOM!');
+ });
+
+ it('with ignored message, calls original console method', () => {
+ testConsole[method](TEST_IGNORED_MESSAGE);
+
+ expect(testConsoleOriginalFn).toHaveBeenCalledTimes(1);
+ expect(testConsoleOriginalFn).toHaveBeenCalledWith(method, TEST_IGNORED_MESSAGE);
+ });
+ });
+
+ describe('with ignoreConsoleMessages', () => {
+ ignoreConsoleMessages([/Hello world .*/]);
+
+ it('adds to ignored messages only for describe block', () => {
+ callConsoleMethods();
+
+ expect(getConsoleCalls()).toEqual([]);
+ });
+ });
+
+ describe('with useConsoleWatcherThrowsImmediately', () => {
+ // eslint-disable-next-line import/no-deprecated
+ useConsoleWatcherThrowsImmediately();
+
+ it('throws when non ignored message', () => {
+ expect(callConsoleMethods).toThrow();
+ });
+ });
+
+ it('with getConsoleCalls, only returns non ignored ones', () => {
+ expect(getConsoleCalls()).toEqual([]);
+
+ callConsoleMethods();
+
+ expect(getConsoleCalls()).toEqual([
+ { method: 'warn', args: ['Hello world warn'] },
+ { method: 'error', args: ['Hello world error'] },
+ ]);
+ });
+
+ it('with forgetConsoleCalls, clears out calls', () => {
+ callConsoleMethods();
+ forgetConsoleCalls();
+
+ expect(getConsoleCalls()).toEqual([]);
+ });
+ });
+
+ describe('setupConsoleWatcher with shouldThrowImmediately', () => {
+ beforeAll(() => {
+ testEnvironment = { global: {} };
+ consoleWatcher = setupConsoleWatcher(testEnvironment, testConsole, {
+ ignores: ['Full message to ignore.', /partial ignore/],
+ shouldThrowImmediately: true,
+ });
+ });
+
+ afterAll(() => {
+ consoleWatcher.dispose();
+ });
+
+ it('does not throw on ignored call', () => {
+ expect(() => testConsole.error(TEST_IGNORED_MESSAGE)).not.toThrow();
+ });
+
+ it('throws when call is not ignored', () => {
+ expect(() => testConsole.error('BLOW UP!')).toThrowErrorMatchingInlineSnapshot(`
+"Unexpected calls to console (1) with:
+
+ [1] error: BLOW UP!
+
+"
+`);
+ });
+ });
+});
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 38214531379..40f2be06e4c 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -8,6 +8,7 @@ const {
} = require('./__helpers__/fake_date/fake_date');
const { TEST_HOST } = require('./__helpers__/test_constants');
const { createGon } = require('./__helpers__/gon_helper');
+const { setupConsoleWatcher } = require('./__helpers__/console_watcher');
class CustomEnvironment extends TestEnvironment {
constructor({ globalConfig, projectConfig }, context) {
@@ -18,35 +19,17 @@ class CustomEnvironment extends TestEnvironment {
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
setGlobalDateToFakeDate();
- const { error: originalErrorFn } = context.console;
- Object.assign(context.console, {
- error(...args) {
- const firstError = args?.[0];
- if (
- typeof firstError === 'string' &&
- ['[Vue warn]: Missing required prop', '[Vue warn]: Invalid prop'].some((line) =>
- firstError.startsWith(line),
- )
- ) {
- originalErrorFn.apply(context.console, args);
- return;
- }
-
- throw new ErrorWithStack(
- `Unexpected call of console.error() with:\n\n${args.join(', ')}`,
- this.error,
- );
- },
-
- warn(...args) {
- if (args?.[0]?.includes('The updateQuery callback for fetchMore is deprecated')) {
- return;
- }
- throw new ErrorWithStack(
- `Unexpected call of console.warn() with:\n\n${args.join(', ')}`,
- this.warn,
- );
- },
+ this.jestConsoleWatcher = setupConsoleWatcher(this, context.console, {
+ ignores: [
+ /The updateQuery callback for fetchMore is deprecated/,
+ // TODO: Remove this and replace with localized calls to `ignoreVueConsoleWarnings`
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/396779#note_1788506238
+ /^\[Vue warn\]: Missing required prop/,
+ /^\[Vue warn\]: Invalid prop/,
+ ],
+ // TODO: Remove this and replace with localized calls to `useConsoleWatcherThrowsImmediately`
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/396779#note_1788506238
+ shouldThrowImmediately: true,
});
const { IS_EE } = projectConfig.testEnvironmentOptions;
@@ -123,6 +106,8 @@ class CustomEnvironment extends TestEnvironment {
// Reset `Date` so that Jest can report timing accurately *roll eyes*...
setGlobalDateToRealDate();
+ this.jestConsoleWatcher.dispose();
+
// eslint-disable-next-line no-restricted-syntax
await new Promise(setImmediate);
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index d3d3e5c8c72..61caa114f2b 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -4,6 +4,7 @@ import { setImmediate } from 'timers';
import Dexie from 'dexie';
import { IDBKeyRange, IDBFactory } from 'fake-indexeddb';
import 'helpers/shared_test_setup';
+import { forgetConsoleCalls, getConsoleCalls, throwErrorFromCalls } from 'helpers/console_watcher';
const indexedDB = new IDBFactory();
@@ -19,6 +20,15 @@ afterEach(() =>
}),
);
+afterEach(() => {
+ const consoleCalls = getConsoleCalls();
+ forgetConsoleCalls();
+
+ if (consoleCalls.length) {
+ throwErrorFromCalls(consoleCalls);
+ }
+});
+
afterEach(async () => {
const dbs = await indexedDB.databases();