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
5 changes: 0 additions & 5 deletions jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ if ( typeof window !== 'undefined' ) {

nock.disableNetConnect();

// Jest runs in standard Node, not Electron. @sentry/electron doesn't work in Node.
jest.mock( '@sentry/electron/main', () => ( {
captureException: jest.fn(),
} ) );

// We consider the app to be online by default.
jest.mock( './src/hooks/use-offline', () => ( {
useOffline: jest.fn().mockReturnValue( false ),
Expand Down
2 changes: 2 additions & 0 deletions src/__mocks__/@sentry/electron/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const addBreadcrumb = jest.fn();
export const captureException = jest.fn();
19 changes: 19 additions & 0 deletions src/__mocks__/electron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const app = {
getFetch: jest.fn(),
getPath: jest.fn( ( name ) => `/path/to/app/${ name }` ),
getName: jest.fn( () => 'App Name' ),
getPreferredSystemLanguages: jest.fn( () => [ 'en-US' ] ),
};

export const BrowserWindow = {
fromWebContents: jest.fn( () => ( {
webContents: {
send: jest.fn(),
},
} ) ),
};

export const shell = {
openExternal: jest.fn(),
trashItem: jest.fn(),
};
40 changes: 40 additions & 0 deletions src/__mocks__/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
type Fs = typeof import('fs');
interface MockedFs extends Fs {
__setFileContents: ( path: string, fileContents: string | string[] ) => void;
}

const fs = jest.createMockFromModule< MockedFs >( 'fs' );
const fsPromises = jest.createMockFromModule< typeof import('fs/promises') >( 'fs/promises' );

fs.promises = fsPromises;

const mockFiles: Record< string, string | string[] > = {};
fs.__setFileContents = ( path: string, fileContents: string | string[] ) => {
mockFiles[ path ] = fileContents;
};
Comment on lines +12 to +14
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourced from the Jest documentation, implemented to allow simultaneously mocking file contents from different locations.


( fs.promises.readFile as jest.Mock ).mockImplementation(
async ( path: string ): Promise< string > => {
const fileContents = mockFiles[ path ];

if ( typeof fileContents === 'string' ) {
return fileContents;
}

return '';
}
);

( fs.promises.readdir as jest.Mock ).mockImplementation(
async ( path: string ): Promise< Array< string > > => {
const dirContents = mockFiles[ path ];

if ( Array.isArray( dirContents ) ) {
return dirContents;
}

return [];
}
);

module.exports = fs;
9 changes: 8 additions & 1 deletion src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,14 @@ export async function createSite(
}

if ( ( await pathExists( path ) ) && ( await isEmptyDir( path ) ) ) {
await createSiteWorkingDirectory( path );
try {
await createSiteWorkingDirectory( path );
} catch ( error ) {
// If site creation failed, remove the generated files and re-throw the
// error so it can be handled by the caller.
shell.trashItem( path );
throw error;
}
Comment on lines +132 to +139
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!, thanks for introducing the logic!

}

const details = {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/tests/locale.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @jest-environment node
*/
Comment on lines +1 to +3
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the node testing environment should provide a more realistic environment for main process tests. Also, the tests should run faster.

import { app } from 'electron';
import { createI18n } from '@wordpress/i18n';
import { getLocaleData, getSupportedLocale } from '../locale';
Expand Down
3 changes: 3 additions & 0 deletions src/lib/tests/passwords.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @jest-environment node
*/
import { createPassword, decodePassword } from '../passwords';

describe( 'createPassword', () => {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/tests/sanitize-for-logging.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @jest-environment node
*/
import { sanitizeForLogging, sanitizeUnstructuredData } from '../sanitize-for-logging';

describe( 'sanitizeForLogging', () => {
Expand Down
10 changes: 3 additions & 7 deletions src/lib/tests/site-language.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
/**
* @jest-environment node
*/
import { app } from 'electron';
import { getPreferredSiteLanguage } from '../site-language';

jest.mock( 'electron', () => ( {
app: {
getPreferredSystemLanguages: jest.fn( () => [ 'en' ] ),
getPath: jest.fn(),
},
} ) );

const originalFetch = global.fetch;

function mockPreferredLanguages( languages: string[] ) {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/tests/sort-sites.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @jest-environment node
*/
// To run tests, execute `npm run test -- src/lib/sort-sites.test.ts` from the root directory
import { sortSites } from '../sort-sites';

Expand Down
30 changes: 12 additions & 18 deletions src/storage/tests/user-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/**
* @jest-environment node
*/
// To run tests, execute `npm run test -- src/storage/user-data.test.ts` from the root directory
import fs from 'fs';
import { loadUserData } from '../user-data';

jest.mock( 'fs' );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to mock Node's built-in modules (e.g.: fs or path), then explicitly calling e.g. jest.mock('path') is required, because built-in modules are not mocked by default.

Jest Manual Mocks documentation


const mockUserData = {
sites: [
{ name: 'Tristan', path: '/to/tristan' },
Expand All @@ -11,23 +16,12 @@ const mockUserData = {
snapshots: [],
};

jest.mock( 'fs', () => ( {
promises: {
readFile: async () => JSON.stringify( mockUserData ),
},
existsSync: () => true,
} ) );

jest.mock( 'electron', () => ( {
app: {
getFetch: jest.fn(),
getPath: jest.fn(),
getName: jest.fn(),
},
} ) );
jest.mock( 'path', () => ( {
join: jest.fn(),
} ) );
( fs as MockedFs ).__setFileContents(
'/path/to/app/appData/App Name/appdata-v1.json',
JSON.stringify( mockUserData )
);
// Assume each site path exists
( fs.existsSync as jest.Mock ).mockReturnValue( true );

describe( 'loadUserData', () => {
test( 'loads user data correctly and sorts sites', async () => {
Expand All @@ -41,7 +35,7 @@ describe( 'loadUserData', () => {
} );

test( 'Filters out sites where the path does not exist', async () => {
fs.existsSync = jest.fn( ( path ) => path === '/to/lancelot' );
( fs.existsSync as jest.Mock ).mockImplementation( ( path ) => path === '/to/lancelot' );
const result = await loadUserData();
expect( result.sites.map( ( sites ) => sites.name ) ).toEqual( [ 'Lancelot' ] );
} );
Expand Down
65 changes: 65 additions & 0 deletions src/tests/ipc-handlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @jest-environment node
*/
import { shell, IpcMainInvokeEvent } from 'electron';
import fs from 'fs';
import { createSite } from '../ipc-handlers';
import { isEmptyDir, pathExists } from '../lib/fs-utils';
import { SiteServer, createSiteWorkingDirectory } from '../site-server';

jest.mock( 'fs' );
jest.mock( '../lib/fs-utils' );
jest.mock( '../site-server' );

( SiteServer.create as jest.Mock ).mockImplementation( ( details ) => ( {
start: jest.fn(),
details,
updateSiteDetails: jest.fn(),
updateCachedThumbnail: jest.fn( () => Promise.resolve() ),
} ) );
( createSiteWorkingDirectory as jest.Mock ).mockResolvedValue( true );

const mockUserData = {
sites: [],
};
( fs as MockedFs ).__setFileContents(
'/path/to/app/appData/App Name/appdata-v1.json',
JSON.stringify( mockUserData )
);
// Assume the provided site path is a directory
( fs.promises.stat as jest.Mock ).mockResolvedValue( {
isDirectory: () => true,
} );

const mockIpcMainInvokeEvent = {} as IpcMainInvokeEvent;

describe( 'createSite', () => {
it( 'should create a site', async () => {
( isEmptyDir as jest.Mock ).mockResolvedValue( true );
( pathExists as jest.Mock ).mockResolvedValue( true );
const [ site ] = await createSite( mockIpcMainInvokeEvent, '/test', 'Test' );

expect( site ).toEqual( {
adminPassword: expect.any( String ),
id: expect.any( String ),
name: 'Test',
path: '/test',
running: false,
} );
} );

describe( 'when the site path started as an empty directory', () => {
it( 'should reset the directory when site creation fails', () => {
( isEmptyDir as jest.Mock ).mockResolvedValue( true );
( pathExists as jest.Mock ).mockResolvedValue( true );
( createSiteWorkingDirectory as jest.Mock ).mockImplementation( () => {
throw new Error( 'Intentional test error' );
} );

createSite( mockIpcMainInvokeEvent, '/test', 'Test' ).catch( () => {
expect( shell.trashItem ).toHaveBeenCalledTimes( 1 );
expect( shell.trashItem ).toHaveBeenCalledWith( '/test' );
} );
} );
} );
} );
7 changes: 0 additions & 7 deletions src/tests/site-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ jest.mock( '../../vendor/wp-now/src', () => ( {
),
} ) );

jest.mock( 'electron', () => ( {
app: {
getPreferredSystemLanguages: jest.fn( () => [ 'en-US' ] ),
getPath: jest.fn( () => '/path/to/app' ),
},
} ) );

describe( 'SiteServer', () => {
describe( 'start', () => {
it( 'should throw if the server starts with a non-WordPress mode', async () => {
Expand Down