Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/contributors/code/testing-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,20 @@ Vitest resets mock implementations and call history, restores spies, resets stub

`wpVitest` is an explicit opt-in for jsdom suites that need hoist-safe helpers inside `vi.hoisted()`.

### Expected console calls

Gutenberg's internal Vitest setup fails a test when `console.error`, `console.warn`, `console.info`, or `console.log` has calls that the test did not explicitly expect. Use `toHaveErroredWith`, `toHaveWarnedWith`, `toHaveInformedWith`, or `toHaveLoggedWith` with specific arguments. Asymmetric matchers such as `expect.objectContaining` are supported.

```js
expect( console ).toHaveWarnedWith( 'The setting is deprecated.' );
```

A successful positive assertion accounts for every matching call already in the mock history, including duplicates. It leaves the history intact, so repeated assertions and separate call-count checks still work. Other calls, including a later call with the same arguments, need their own assertion. Negative assertions and failed assertions do not account for any calls.

The argument-free matchers, such as `toHaveWarned()`, remain supported for compatibility and account for all calls to that method already in the history. A broad assertion can therefore still hide an unrelated call. Prefer specific arguments so only matching calls are accounted for. Standard spy assertions such as `toHaveBeenCalledWith` do not account for console calls in this helper.

The check runs after the test and its cleanup hooks, so assertions in `afterEach` are supported. Clearing, resetting, or restoring a mock does not excuse unaccounted calls. Assert expected calls before clearing their history. Accounting starts fresh for each test, including after a failed check.

### Mocking dependencies

#### Dependency injection
Expand Down
14 changes: 2 additions & 12 deletions packages/abilities/src/tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,6 @@ describe( 'API functions', () => {
} );

it( 'should handle errors in client ability execution', async () => {
const consoleErrorSpy = vi
.spyOn( console, 'error' )
.mockImplementation( () => {} );
const executionError = new Error( 'Execution failed' );
const mockCallback = vi.fn().mockRejectedValue( executionError );

Expand All @@ -420,18 +417,13 @@ describe( 'API functions', () => {
executeAbility( 'test/client-ability', {} )
).rejects.toThrow( 'Execution failed' );

expect( consoleErrorSpy ).toHaveBeenCalledWith(
expect( console ).toHaveErroredWith(
'Error executing ability test/client-ability:',
executionError
);

consoleErrorSpy.mockRestore();
} );

it( 'should handle errors in server ability execution', async () => {
const consoleErrorSpy = vi
.spyOn( console, 'error' )
.mockImplementation( () => {} );
const serverError = new Error( 'Server execution failed' );
const mockServerCallback = vi.fn().mockRejectedValue( serverError );

Expand All @@ -455,12 +447,10 @@ describe( 'API functions', () => {
executeAbility( 'test/server-ability', {} )
).rejects.toThrow( 'Server execution failed' );

expect( consoleErrorSpy ).toHaveBeenCalledWith(
expect( console ).toHaveErroredWith(
'Error executing ability test/server-ability:',
serverError
);

consoleErrorSpy.mockRestore();
} );

it( 'should execute ability without callback as server ability', async () => {
Expand Down
67 changes: 11 additions & 56 deletions packages/abilities/src/tests/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Tests for schema validation utilities.
*/

import { describe, expect, it, vi } from 'vitest';
import { describe, expect, it } from 'vitest';
import { validateValueFromSchema } from '../validation';

describe( 'validateValueFromSchema', () => {
Expand Down Expand Up @@ -135,37 +135,23 @@ describe( 'validateValueFromSchema', () => {

describe( 'edge cases', () => {
it( 'should pass validation when empty schema provided', () => {
const consoleSpy = vi
.spyOn( console, 'warn' )
.mockImplementation( () => {} );

expect( validateValueFromSchema( 'anything', {} ) ).toBe( true );
expect( consoleSpy ).toHaveBeenCalledWith(
expect( console ).toHaveWarnedWith(
'The "type" schema keyword for value is required.'
);

consoleSpy.mockRestore();
} );

it( 'should warn when type is missing but still pass validation', () => {
const consoleSpy = vi
.spyOn( console, 'warn' )
.mockImplementation( () => {} );
const schema = { properties: { name: { type: 'string' } } };
const result = validateValueFromSchema( { name: 'test' }, schema );

expect( result ).toBe( true );
expect( consoleSpy ).toHaveBeenCalledWith(
expect( console ).toHaveWarnedWith(
'The "type" schema keyword for value is required.'
);

consoleSpy.mockRestore();
} );

it( 'should include param name in warning when provided', () => {
const consoleSpy = vi
.spyOn( console, 'warn' )
.mockImplementation( () => {} );
const schema = { format: 'email' }; // Schema without type
const result = validateValueFromSchema(
'test@example.com',
Expand All @@ -174,11 +160,9 @@ describe( 'validateValueFromSchema', () => {
);

expect( result ).toBe( true );
expect( consoleSpy ).toHaveBeenCalledWith(
expect( console ).toHaveWarnedWith(
'The "type" schema keyword for email_field is required.'
);

consoleSpy.mockRestore();
} );

it( 'should handle null values correctly', () => {
Expand Down Expand Up @@ -451,76 +435,57 @@ describe( 'validateValueFromSchema', () => {

describe( 'schema edge cases and errors', () => {
it( 'should handle empty schema object as valid but warn about missing type', () => {
const consoleSpy = vi
.spyOn( console, 'warn' )
.mockImplementation( () => {} );

// Empty object schema triggers warning about missing type
expect( validateValueFromSchema( 'anything', {} ) ).toBe( true );
expect( consoleSpy ).toHaveBeenCalledWith(
expect( console ).toHaveWarnedWith(
'The "type" schema keyword for value is required.'
);

consoleSpy.mockRestore();
} );

it( 'should warn for invalid schema types but still pass validation', () => {
const consoleSpy = vi
.spyOn( console, 'warn' )
.mockImplementation( () => {} );

// Testing edge cases where schema is not a valid object
expect(
validateValueFromSchema(
'anything',
undefined as unknown as Record< string, any >
)
).toBe( true );
expect( consoleSpy ).toHaveBeenCalledWith(
expect( console ).toHaveWarnedWith(
'Schema must be an object. Received undefined.'
);

consoleSpy.mockClear();
expect(
validateValueFromSchema(
123,
null as unknown as Record< string, any >
)
).toBe( true );
expect( consoleSpy ).toHaveBeenCalledWith(
expect( console ).toHaveWarnedWith(
'Schema must be an object. Received object.' // typeof null === 'object'
);

consoleSpy.mockClear();
expect(
validateValueFromSchema(
true,
false as unknown as Record< string, any >
)
).toBe( true );
expect( consoleSpy ).toHaveBeenCalledWith(
expect( console ).toHaveWarnedWith(
'Schema must be an object. Received boolean.'
);

consoleSpy.mockRestore();
} );

it( 'should handle schema compilation errors', () => {
// Pass an invalid schema that will cause compilation error
const invalidSchema = { type: 'invalid-type' };
const consoleErrorSpy = vi
.spyOn( console, 'error' )
.mockImplementation( () => {} );

const result = validateValueFromSchema( 'test', invalidSchema );

expect( result ).toBe( 'Invalid schema provided for validation.' );
expect( consoleErrorSpy ).toHaveBeenCalledWith(
expect( console ).toHaveErroredWith(
'Schema compilation error:',
expect.any( Error )
);

consoleErrorSpy.mockRestore();
} );

it( 'should handle const validation keyword', () => {
Expand Down Expand Up @@ -568,19 +533,14 @@ describe( 'validateValueFromSchema', () => {
},
},
};
const consoleErrorSpy = vi
.spyOn( console, 'error' )
.mockImplementation( () => {} );

expect(
validateValueFromSchema( { name: 'hello' }, schema )
).toBe( 'Invalid schema provided for validation.' );
expect( consoleErrorSpy ).toHaveBeenCalledWith(
expect( console ).toHaveErroredWith(
'Schema compilation error:',
expect.any( Error )
);

consoleErrorSpy.mockRestore();
}
);

Expand All @@ -589,19 +549,14 @@ describe( 'validateValueFromSchema', () => {
type: 'string',
sanitize_callback: 'sanitize_text_field',
};
const consoleErrorSpy = vi
.spyOn( console, 'error' )
.mockImplementation( () => {} );

expect( validateValueFromSchema( 'hello', schema ) ).toBe(
'Invalid schema provided for validation.'
);
expect( consoleErrorSpy ).toHaveBeenCalledWith(
expect( console ).toHaveErroredWith(
'Schema compilation error:',
expect.any( Error )
);

consoleErrorSpy.mockRestore();
} );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe( 'ChildLayoutControl', () => {
} );
} );

it( 'shows legacy fixed flex sizing as max', () => {
it( 'shows legacy fixed flex sizing as max', async () => {
renderControl( {
parentLayout: {
type: 'flex',
Expand All @@ -132,7 +132,9 @@ describe( 'ChildLayoutControl', () => {
},
} );

expect( screen.getByRole( 'radio', { name: 'Max' } ) ).toBeChecked();
expect(
await screen.findByRole( 'radio', { name: 'Max' } )
).toBeChecked();
} );

it( 'sets fixedNoShrink when selecting fixed flex sizing', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ImageSizeControl from '../index';

Expand All @@ -16,7 +16,7 @@ describe( 'ImageSizeControl', () => {
vi.clearAllMocks();
} );

it( 'returns custom dimensions when they exist', () => {
it( 'returns custom dimensions when they exist', async () => {
render(
<ImageSizeControl
imageHeight="100"
Expand All @@ -29,14 +29,14 @@ describe( 'ImageSizeControl', () => {
);

expect(
screen.getByRole( 'spinbutton', { name: 'Height' } )
await screen.findByRole( 'spinbutton', { name: 'Height' } )
).toHaveValue( 300 );
expect(
screen.getByRole( 'spinbutton', { name: 'Width' } )
).toHaveValue( 400 );
} );

it( 'returns default dimensions when custom dimensions are undefined', () => {
it( 'returns default dimensions when custom dimensions are undefined', async () => {
render(
<ImageSizeControl
imageHeight="100"
Expand All @@ -46,32 +46,32 @@ describe( 'ImageSizeControl', () => {
);

expect(
screen.getByRole( 'spinbutton', { name: 'Height' } )
await screen.findByRole( 'spinbutton', { name: 'Height' } )
).toHaveValue( 100 );
expect(
screen.getByRole( 'spinbutton', { name: 'Width' } )
).toHaveValue( 200 );
} );

it( 'returns no value when custom and default dimensions are undefined', () => {
it( 'returns no value when custom and default dimensions are undefined', async () => {
render( <ImageSizeControl onChange={ mockOnChange } /> );

expect(
screen.getByRole( 'spinbutton', { name: 'Height' } )
await screen.findByRole( 'spinbutton', { name: 'Height' } )
).toHaveValue( null );
expect(
screen.getByRole( 'spinbutton', { name: 'Width' } )
).toHaveValue( null );
} );

it( 'returns default dimensions when initially undefined defaults are defined on rerender', () => {
it( 'returns default dimensions when initially undefined defaults are defined on rerender', async () => {
// Simulates an initial render with custom and default dimensions undefined.
// This occurs when an image is uploaded for the first time, for example.
const { rerender } = render(
<ImageSizeControl onChange={ mockOnChange } />
);

const heightInput = screen.getByRole( 'spinbutton', {
const heightInput = await screen.findByRole( 'spinbutton', {
name: 'Height',
} );
const widthInput = screen.getByRole( 'spinbutton', { name: 'Width' } );
Expand All @@ -91,7 +91,7 @@ describe( 'ImageSizeControl', () => {
);

// The dimensions should update to the defaults.
expect( heightInput ).toHaveValue( 300 );
await waitFor( () => expect( heightInput ).toHaveValue( 300 ) );
expect( widthInput ).toHaveValue( 400 );
} );

Expand Down Expand Up @@ -263,7 +263,7 @@ describe( 'ImageSizeControl', () => {
{ value: 'large', label: 'Large' },
];

it( 'displays the selected slug', () => {
it( 'displays the selected slug', async () => {
render(
<ImageSizeControl
imageSizeOptions={ IMAGE_SIZE_OPTIONS }
Expand All @@ -274,7 +274,7 @@ describe( 'ImageSizeControl', () => {
);

expect(
screen.getByRole( 'combobox', { name: 'Resolution' } )
await screen.findByRole( 'combobox', { name: 'Resolution' } )
).toHaveValue( 'medium' );
} );

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import { useRegistry, useSelect } from '@wordpress/data';
import { BlockEditorProvider, ExperimentalBlockEditorProvider } from '../';
Expand Down Expand Up @@ -64,10 +64,6 @@ describe( 'BlockEditorProvider', () => {
expect( settings ).toHaveProperty( 'stableSetting' );
} );
it( 'preserves deprecated getters incoming from the settings reducer', async () => {
const consoleWarn = vi
.spyOn( global.console, 'warn' )
.mockImplementation( () => {} );

const { container } = render(
<BlockEditorProvider
settings={ {
Expand All @@ -85,11 +81,9 @@ describe( 'BlockEditorProvider', () => {
} )
);

expect( consoleWarn ).toHaveBeenCalledWith(
expect( console ).toHaveWarnedWith(
'__unstableIsPreviewMode is deprecated since version 6.8. Please use isPreviewMode instead.'
);

consoleWarn.mockRestore();
} );
} );

Expand Down
Loading
Loading