Skip to content

Commit

Permalink
Add support for (before|after)(Each|All) methods
Browse files Browse the repository at this point in the history
Summary: Add capability to setup / teardown tests

Differential Revision: D68454176
  • Loading branch information
andrewdacenko authored and facebook-github-bot committed Jan 21, 2025
1 parent 9afad52 commit b04511e
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 46 deletions.
206 changes: 160 additions & 46 deletions packages/react-native-fantom/runtime/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import nullthrows from 'nullthrows';
import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom';

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

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

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

const ancestorTitles: Array<string> = [];
type Context =
| /* child context */ {
title: string,
parent: Context,
afterAll: Hook[],
afterEach: Hook[],
beforeAll: Hook[],
beforeEach: Hook[],
specs: Spec[],
children: Context[],
}
| /* root context */ {
afterAll: Hook[],
afterEach: Hook[],
beforeAll: Hook[],
beforeEach: Hook[],
specs: Spec[],
children: Context[],
};

let currentContext: Context = {
beforeAll: [],
beforeEach: [],
afterAll: [],
afterEach: [],
specs: [],
children: [],
};

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

const globalDescribe = (global.describe = (
title: string,
implementation: () => mixed,
) => {
ancestorTitles.push(title);
const parentContext = currentContext;
const childContext: Context = {
title,
parent: parentContext,
afterAll: [],
afterEach: [],
beforeAll: [],
beforeEach: [],
specs: [],
children: [],
};
currentContext.children.push(childContext);
currentContext = childContext;
implementation();
ancestorTitles.pop();
currentContext = parentContext;
});

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

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

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

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

const globalIt =
(global.it =
global.test =
(title: string, implementation: () => mixed) =>
tests.push({
currentContext.specs.push({
title,
implementation,
ancestorTitles: ancestorTitles.slice(),
isFocused:
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'focused',
Expand Down Expand Up @@ -142,52 +196,112 @@ function runWithGuard(fn: () => void) {
}
}

function isFocusedRun(context: Context): boolean {
return (
context.specs.some(spec => spec.isFocused) ||
context.children.some(isFocusedRun)
);
}

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);

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 hasFocusedTests = isFocusedRun(currentContext);

test.result = result;
snapshotContext.setTargetTest(result.fullName);
const results: Array<TestCaseResult> = [];
executeSpecs(currentContext);
reportTestSuiteResult({
testResults: results,
});

if (!test.isSkipped && (!hasFocusedTests || test.isFocused)) {
let status;
let error;
function invokeHooks(context: Context, hookType: 'beforeEach' | 'afterEach') {
const contextStack = [];
let current: ?Context = context;
while (current != null) {
contextStack.push(current);
current = current.parent;
}

const start = Date.now();
if (hookType === 'beforeEach') {
contextStack.reverse();
}

try {
test.implementation();
status = 'passed';
} catch (e) {
error = e;
status = 'failed';
for (const c of contextStack) {
for (const hook of c[hookType]) {
hook();
}
}
}

function executeSpecs(context: Context) {
for (const setup of context.beforeAll) {
setup();
}

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

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

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

const start = Date.now();

try {
invokeHooks(context, 'beforeEach');

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

result.snapshotResults = snapshotContext.getSnapshotResults();
invokeHooks(context, 'afterEach');

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();

results.push(result);
}
}
}

reportTestSuiteResult({
testResults: tests.map(test => nullthrows(test.result)),
});
for (const childContext of context.children) {
executeSpecs(childContext);
}

for (const teardown of context.afterAll) {
teardown();
}
}
}

function reportTestSuiteResult(testSuiteResult: TestSuiteResult): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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[] = [];

beforeAll(() => {
logs.push('root - beforeAll');
});

afterAll(() => {
logs.push('root - afterAll');
});

beforeEach(() => {
logs.push('root - beforeEach');
});

afterEach(() => {
logs.push('root - afterEach');
});

test('root hooks', () => {
logs.push('root - test');
expect(logs).toMatchSnapshot();
});

describe('FantomSetupHooks', () => {
beforeAll(() => {
logs.push('FantomSetupHooks - beforeAll');
});

afterAll(() => {
logs.push('FantomSetupHooks - afterAll');
});

beforeEach(() => {
logs.push('FantomSetupHooks - beforeEach');
});

afterEach(() => {
logs.push('FantomSetupHooks - afterEach');
});

test('first', () => {
logs.push('FantomSetupHooks - first');
expect(logs).toMatchSnapshot();
});

test('second', () => {
logs.push('FantomSetupHooks - second');
expect(logs).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FantomSetupHooks first 1`] = `
Array [
"root - beforeAll",
"root - beforeEach",
"root - test",
"root - afterEach",
"FantomSetupHooks - beforeAll",
"root - beforeEach",
"FantomSetupHooks - beforeEach",
"FantomSetupHooks - first",
]
`;

exports[`FantomSetupHooks second 1`] = `
Array [
"root - beforeAll",
"root - beforeEach",
"root - test",
"root - afterEach",
"FantomSetupHooks - beforeAll",
"root - beforeEach",
"FantomSetupHooks - beforeEach",
"FantomSetupHooks - first",
"FantomSetupHooks - afterEach",
"root - afterEach",
"root - beforeEach",
"FantomSetupHooks - beforeEach",
"FantomSetupHooks - second",
]
`;

exports[`root hooks 1`] = `
Array [
"root - beforeAll",
"root - beforeEach",
"root - test",
]
`;

0 comments on commit b04511e

Please sign in to comment.