Skip to content
Closed
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
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