Skip to content

Commit

Permalink
[skip ci] Add support for (before|after)(Each|All) methods (#48820)
Browse files Browse the repository at this point in the history
Summary:

Changelog: [Internal]
Add capability to setup / teardown tests

Differential Revision: D68454176
  • Loading branch information
andrewdacenko authored and facebook-github-bot committed Jan 22, 2025
1 parent d154cd5 commit 7e7659c
Show file tree
Hide file tree
Showing 4 changed files with 319 additions and 29 deletions.
201 changes: 172 additions & 29 deletions packages/react-native-fantom/runtime/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import nullthrows from 'nullthrows';
import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom';

export type TestCaseResult = {
ancestorTitles: Array<string>,
title: string,
ancestorTitles: Array<string>,
fullName: string,
status: 'passed' | 'failed' | 'pending',
duration: number,
Expand All @@ -40,43 +40,111 @@ export type TestSuiteResult =
},
};

const tests: Array<{
type Hook = () => void;

type Spec = {
title: string,
ancestorTitles: Array<string>,
implementation: () => mixed,
context: Context,
isFocused: boolean,
isSkipped: boolean,
implementation: () => mixed,
result?: TestCaseResult,
}> = [];
};

type Suite = Spec | Context;

const ancestorTitles: Array<string> = [];
type RootContext = {
afterAllHooks: Hook[],
afterEachHooks: Hook[],
beforeAllHooks: Hook[],
beforeEachHooks: Hook[],
children: Array<Suite>,
isFocused: boolean,
isSkipped: boolean,
};

type Context =
| RootContext
| /* child context */ {
...RootContext,
title: string,
parent: Context,
};

let currentContext: Context = {
beforeAllHooks: [],
beforeEachHooks: [],
afterAllHooks: [],
afterEachHooks: [],
children: [],
isFocused: false,
isSkipped: false,
};

const globalModifiers: Array<'focused' | 'skipped'> = [];

const globalDescribe = (global.describe = (
title: string,
implementation: () => mixed,
) => {
ancestorTitles.push(title);
const parentContext = currentContext;
const {focused, skipped} = getFocusState();
const childContext: Context = {
title,
parent: parentContext,
afterAllHooks: [],
afterEachHooks: [],
beforeAllHooks: [],
beforeEachHooks: [],
children: [],
isFocused: focused,
isSkipped: skipped,
};
currentContext.children.push(childContext);
currentContext = childContext;
implementation();
ancestorTitles.pop();
currentContext = parentContext;
});

global.afterAll = (implementation: () => void) => {
currentContext.afterAllHooks.push(implementation);
};

global.afterEach = (implementation: () => void) => {
currentContext.afterEachHooks.push(implementation);
};

global.beforeAll = (implementation: () => void) => {
currentContext.beforeAllHooks.push(implementation);
};

global.beforeEach = (implementation: () => void) => {
currentContext.beforeEachHooks.push(implementation);
};

function getFocusState(): {focused: boolean, skipped: boolean} {
const focused =
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'focused';
const skipped =
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'skipped';
return {focused, skipped};
}

const globalIt =
(global.it =
global.test =
(title: string, implementation: () => mixed) =>
tests.push({
(title: string, implementation: () => mixed) => {
const {focused, skipped} = getFocusState();
currentContext.children.push({
title,
context: currentContext,
implementation,
ancestorTitles: ancestorTitles.slice(),
isFocused:
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'focused',
isSkipped:
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'skipped',
}));
isFocused: focused,
isSkipped: skipped,
});
});

// $FlowExpectedError[prop-missing]
global.fdescribe = global.describe.only = (
Expand Down Expand Up @@ -142,32 +210,88 @@ function runWithGuard(fn: () => void) {
}
}

function isFocusedRun(context: Suite): boolean {
if (context.isFocused) {
return true;
}

if ('children' in context) {
return context.children.some(isFocusedRun);
}

return false;
}

function getContextTitle(context: Context): string[] {
if (context.parent == null) {
return [];
}

const titles = [context.title];
if (context.parent) {
titles.push(...getContextTitle(context.parent));
}
return titles.reverse();
}

function executeTests() {
const hasFocusedTests = tests.some(test => test.isFocused);
const hasFocusedTests = isFocusedRun(currentContext);

const results: Array<TestCaseResult> = [];
executeSuite(currentContext);
reportTestSuiteResult({
testResults: results,
});

function invokeHooks(
context: Context,
hookType: 'beforeEachHooks' | 'afterEachHooks',
) {
const contextStack = [];
let current: ?Context = context;
while (current != null) {
if (hookType === 'beforeEachHooks') {
contextStack.unshift(current);
} else {
contextStack.push(current);
}
current = current.parent;
}

for (const c of contextStack) {
for (const hook of c[hookType]) {
hook();
}
}
}

for (const test of tests) {
function executeSpec(spec: Spec) {
const ancestorTitles = getContextTitle(spec.context);
const result: TestCaseResult = {
title: test.title,
fullName: [...test.ancestorTitles, test.title].join(' '),
ancestorTitles: test.ancestorTitles,
title: spec.title,
ancestorTitles,
fullName: [...ancestorTitles, spec.title].join(' '),
status: 'pending',
duration: 0,
failureMessages: [],
numPassingAsserts: 0,
snapshotResults: {},
};

test.result = result;
spec.result = result;
snapshotContext.setTargetTest(result.fullName);

if (!test.isSkipped && (!hasFocusedTests || test.isFocused)) {
if (!spec.isSkipped && (!hasFocusedTests || spec.isFocused)) {
let status;
let error;

const start = Date.now();

try {
test.implementation();
invokeHooks(spec.context, 'beforeEachHooks');
spec.implementation();
invokeHooks(spec.context, 'afterEachHooks');

status = 'passed';
} catch (e) {
error = e;
Expand All @@ -183,11 +307,30 @@ function executeTests() {

result.snapshotResults = snapshotContext.getSnapshotResults();
}
results.push(result);
}

reportTestSuiteResult({
testResults: tests.map(test => nullthrows(test.result)),
});
function executeContext(context: Context) {
for (const beforeAllHook of context.beforeAllHooks) {
beforeAllHook();
}

for (const child of context.children) {
executeSuite(child);
}

for (const afterAllHook of context.afterAllHooks) {
afterAllHook();
}
}

function executeSuite(suite: Suite) {
if ('children' in suite) {
executeContext(suite);
} else {
executeSpec(suite);
}
}
}

function reportTestSuiteResult(testSuiteResult: TestSuiteResult): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/

const logs: string[] = [];
const log = (message: string): void => {
logs.push(message);
};

afterAll(() => {
// Source https://jestjs.io/docs/setup-teardown#order-of-execution
expect(logs).toEqual([
'describe outer-a',
'describe inner 1',
'describe outer-b',
'describe inner 2',
'describe outer-c',
'test 1',
'test 2',
'test 3',
]);
});

describe('describe outer', () => {
log('describe outer-a');

describe('describe inner 1', () => {
log('describe inner 1');

test('test 1', () => log('test 1'));
});

log('describe outer-b');

test('test 2', () => log('test 2'));

describe('describe inner 2', () => {
log('describe inner 2');

test('test 3', () => log('test 3'));
});

log('describe outer-c');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/

const logs: string[] = [];
const log = (message: string): void => {
logs.push(message);
};

beforeEach(() => log('connection setup'));
beforeEach(() => log('database setup'));

afterEach(() => log('database teardown'));
afterEach(() => log('connection teardown'));
afterAll(() => {
// Source https://jestjs.io/docs/setup-teardown#order-of-execution
expect(logs).toEqual([
'connection setup',
'database setup',
'test 1',
'database teardown',
'connection teardown',

'connection setup',
'database setup',
'extra database setup',
'test 2',
'extra database teardown',
'database teardown',
'connection teardown',
]);
});

test('test 1', () => log('test 1'));

describe('extra', () => {
beforeEach(() => log('extra database setup'));
afterEach(() => log('extra database teardown'));

test('test 2', () => log('test 2'));
});
Loading

0 comments on commit 7e7659c

Please sign in to comment.