Skip to content

Commit

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

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

Reviewed By: rubennorte

Differential Revision: D68454176
  • Loading branch information
andrewdacenko authored and facebook-github-bot committed Jan 23, 2025
1 parent 317f130 commit 47d03e8
Show file tree
Hide file tree
Showing 5 changed files with 479 additions and 58 deletions.
307 changes: 249 additions & 58 deletions packages/react-native-fantom/runtime/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type {SnapshotConfig, TestSnapshotResults} from './snapshotContext';
import expect from './expect';
import {createMockFunction} from './mocks';
import {setupSnapshotConfig, snapshotContext} from './snapshotContext';
import nullthrows from 'nullthrows';
import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom';

export type TestCaseResult = {
Expand All @@ -40,43 +39,108 @@ export type TestSuiteResult =
},
};

const tests: Array<{
type FocusState = {
focused: boolean,
skipped: boolean,
};

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

type Suite = Spec | Context;

const ancestorTitles: Array<string> = [];
type Hook = () => void;

type Context = {
...FocusState,
title?: string,
afterAllHooks: Hook[],
afterEachHooks: Hook[],
beforeAllHooks: Hook[],
beforeEachHooks: Hook[],
parentContext?: Context,
children: Array<Suite>,
};

const rootContext: Context = {
beforeAllHooks: [],
beforeEachHooks: [],
afterAllHooks: [],
afterEachHooks: [],
children: [],
focused: false,
skipped: false,
};
let currentContext: Context = rootContext;

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,
parentContext,
afterAllHooks: [],
afterEachHooks: [],
beforeAllHooks: [],
beforeEachHooks: [],
children: [],
focused: focused,
skipped: 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,
parentContext: currentContext,
implementation,
ancestorTitles: ancestorTitles.slice(),
isFocused:
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'focused',
isSkipped:
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'skipped',
}));
focused: focused,
skipped: skipped,
});
});

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

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

for (const test of tests) {
const result: TestCaseResult = {
title: test.title,
fullName: [...test.ancestorTitles, test.title].join(' '),
ancestorTitles: test.ancestorTitles,
status: 'pending',
duration: 0,
failureMessages: [],
numPassingAsserts: 0,
snapshotResults: {},
};
const focusCache = new Map<Suite, boolean>();

function isFocusedSuite(suite: Suite): boolean {
const cached = focusCache.get(suite);
if (cached != null) {
return cached;
}

if (isSkipped(suite)) {
focusCache.set(suite, false);
return false;
}

if ('children' in suite) {
const hasFocused = suite.children.some(isFocusedSuite);
focusCache.set(suite, hasFocused);
return hasFocused;
}

focusCache.set(suite, suite.focused);
return suite.focused;
}

test.result = result;
snapshotContext.setTargetTest(result.fullName);
const skippedCache = new Map<Suite, boolean>();
function isSkipped(suite: Suite): boolean {
const cached = skippedCache.get(suite);
if (cached != null) {
return cached;
}

if (!test.isSkipped && (!hasFocusedTests || test.isFocused)) {
let status;
let error;
if (suite.skipped) {
skippedCache.set(suite, true);
return true;
}

const start = Date.now();
if (suite.parentContext != null) {
const skipped = isSkipped(suite.parentContext);
skippedCache.set(suite, skipped);
return skipped;
}

skippedCache.set(suite, false);
return false;
}

try {
test.implementation();
status = 'passed';
} catch (e) {
error = e;
status = 'failed';
}
function getContextTitle(context: Context): string[] {
if (context.parentContext == null) {
return [];
}

result.status = status;
result.duration = Date.now() - start;
result.failureMessages =
status === 'failed' && error
? [error.stack ?? error.message ?? String(error)]
: [];
const titles = context.title != null ? [context.title] : [];
if (context.parentContext) {
titles.push(...getContextTitle(context.parentContext));
}
return titles.reverse();
}

result.snapshotResults = snapshotContext.getSnapshotResults();
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.parentContext;
}

reportTestSuiteResult({
testResults: tests.map(test => nullthrows(test.result)),
});
for (const c of contextStack) {
for (const hook of c[hookType]) {
hook();
}
}
}

function shouldRunSuite(suite: Suite): boolean {
if (isSkipped(suite)) {
return false;
}

if (isFocusedSuite(suite)) {
return true;
}

// there is a focused suite in the root at some point
// but not in this suite hence we should not run it
if (isFocusedSuite(rootContext)) {
return false;
}

return true;
}

function runSpec(spec: Spec): TestCaseResult {
const ancestorTitles = getContextTitle(spec.parentContext);
const result: TestCaseResult = {
title: spec.title,
ancestorTitles,
fullName: [...ancestorTitles, spec.title].join(' '),
status: 'pending',
duration: 0,
failureMessages: [],
numPassingAsserts: 0,
snapshotResults: {},
};

if (!shouldRunSuite(spec)) {
return result;
}

let status;
let error;

const start = Date.now();
snapshotContext.setTargetTest(result.fullName);

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

status = 'passed';
} catch (e) {
error = e;
status = 'failed';
}

result.status = status;
result.duration = Date.now() - start;
result.failureMessages =
status === 'failed' && error
? [error.stack ?? error.message ?? String(error)]
: [];

result.snapshotResults = snapshotContext.getSnapshotResults();
return result;
}

function runContext(context: Context): TestCaseResult[] {
const shouldRunHooks = shouldRunSuite(context);

if (shouldRunHooks) {
for (const beforeAllHook of context.beforeAllHooks) {
beforeAllHook();
}
}

const testResults: TestCaseResult[] = [];
for (const child of context.children) {
testResults.push(...runSuite(child));
}

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

return testResults;
}

function runSuite(suite: Suite): TestCaseResult[] {
if ('children' in suite) {
return runContext(suite);
} else {
return [runSpec(suite)];
}
}

function reportTestSuiteResult(testSuiteResult: TestSuiteResult): void {
Expand All @@ -200,7 +389,9 @@ function reportTestSuiteResult(testSuiteResult: TestSuiteResult): void {
}

global.$$RunTests$$ = () => {
executeTests();
reportTestSuiteResult({
testResults: runSuite(currentContext),
});
};

export function registerTest(
Expand Down
Loading

0 comments on commit 47d03e8

Please sign in to comment.