mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
* chore(ci): make all firestore integration tests optional * fix: align expressions and pipelines, address review comments, update yarn.lock, and fix package versions * chore: update yarn.lock after merge * fix: restore firestore package version and devDependencies to 4.15.0
332 lines
10 KiB
TypeScript
332 lines
10 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import stringify from 'json-stable-stringify';
|
|
import { ExclusiveTestFunction, PendingTestFunction } from 'mocha';
|
|
|
|
import { Pipeline } from '../../../lite/pipelines/pipelines';
|
|
import { pipelineEq } from '../../../src/core/pipeline-util';
|
|
import { queryEquals, QueryImpl } from '../../../src/core/query';
|
|
import { targetEquals, TargetImpl } from '../../../src/core/target';
|
|
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
|
|
import { debugAssert } from '../../../src/util/assert';
|
|
import { primitiveComparator } from '../../../src/util/misc';
|
|
import { addEqualityMatcher } from '../../util/equality_matcher';
|
|
|
|
import { SpecBuilder } from './spec_builder';
|
|
import { SpecStep } from './spec_test_runner';
|
|
|
|
// Disables all other tests; useful for debugging. Multiple tests can have
|
|
// this tag and they'll all be run (but all others won't).
|
|
const EXCLUSIVE_TAG = 'exclusive';
|
|
// Explicit per-platform disable flags.
|
|
const NO_WEB_TAG = 'no-web';
|
|
const NO_ANDROID_TAG = 'no-android';
|
|
const NO_IOS_TAG = 'no-ios';
|
|
// The remaining tags specify features that must be present to run a given test
|
|
// Multi-client related tests (which imply persistence).
|
|
export const MULTI_CLIENT_TAG = 'multi-client';
|
|
const EAGER_GC_TAG = 'eager-gc';
|
|
const DURABLE_PERSISTENCE_TAG = 'durable-persistence';
|
|
const BENCHMARK_TAG = 'benchmark';
|
|
const SKIP_PIPELINE_CONVERSION = 'no-pipeline-conversion';
|
|
const KNOWN_TAGS = [
|
|
BENCHMARK_TAG,
|
|
EXCLUSIVE_TAG,
|
|
MULTI_CLIENT_TAG,
|
|
NO_WEB_TAG,
|
|
NO_ANDROID_TAG,
|
|
NO_IOS_TAG,
|
|
EAGER_GC_TAG,
|
|
DURABLE_PERSISTENCE_TAG,
|
|
SKIP_PIPELINE_CONVERSION
|
|
];
|
|
|
|
// TODO(mrschmidt): Make this configurable with mocha options.
|
|
const RUN_BENCHMARK_TESTS = false;
|
|
const BENCHMARK_TEST_TIMEOUT_MS = 10 * 1000;
|
|
|
|
// The format of one describeSpec written to a JSON file.
|
|
interface SpecOutputFormat {
|
|
describeName: string;
|
|
itName: string;
|
|
tags: string[];
|
|
comment?: string;
|
|
steps: SpecStep[];
|
|
}
|
|
|
|
// The name of the describeSpec that's currently running.
|
|
let describeName = '';
|
|
|
|
// Tags for the describeSpec that's current running.
|
|
let describeTags: string[] = [];
|
|
|
|
// A map of string name -> spec json for every `it` in this `describe`.
|
|
let specsInThisTest: { [name: string]: SpecOutputFormat };
|
|
|
|
// A function to write the specs with, if set.
|
|
let writeJSONFile: ((json: string) => void) | null = null;
|
|
|
|
/**
|
|
* If you call this function before your describeSpec, then the spec test will
|
|
* be written using the given function instead of running as a normal test.
|
|
*/
|
|
export function setSpecJSONHandler(writer: (json: string) => void): void {
|
|
writeJSONFile = writer;
|
|
}
|
|
|
|
/** Gets the test runner based on the specified tags. */
|
|
function getTestRunner(
|
|
tags: string[],
|
|
persistenceEnabled: boolean,
|
|
convertToPipeline: boolean
|
|
): ExclusiveTestFunction | PendingTestFunction {
|
|
if (tags.indexOf(NO_WEB_TAG) >= 0) {
|
|
// eslint-disable-next-line no-restricted-properties
|
|
return it.skip;
|
|
} else if (
|
|
!persistenceEnabled &&
|
|
tags.indexOf(DURABLE_PERSISTENCE_TAG) !== -1
|
|
) {
|
|
// Test requires actual persistence, but it's not enabled. Skip it.
|
|
// eslint-disable-next-line no-restricted-properties
|
|
return it.skip;
|
|
} else if (persistenceEnabled && tags.indexOf(EAGER_GC_TAG) !== -1) {
|
|
// spec should have a comment explaining why it is being skipped.
|
|
// eslint-disable-next-line no-restricted-properties
|
|
return it.skip;
|
|
} else if (!persistenceEnabled && tags.indexOf(MULTI_CLIENT_TAG) !== -1) {
|
|
// eslint-disable-next-line no-restricted-properties
|
|
return it.skip;
|
|
} else if (tags.indexOf(BENCHMARK_TAG) >= 0 && !RUN_BENCHMARK_TESTS) {
|
|
// eslint-disable-next-line no-restricted-properties
|
|
return it.skip;
|
|
} else if (convertToPipeline && tags.indexOf(SKIP_PIPELINE_CONVERSION) >= 0) {
|
|
// eslint-disable-next-line no-restricted-properties
|
|
return it.skip;
|
|
} else if (tags.indexOf(EXCLUSIVE_TAG) >= 0) {
|
|
// eslint-disable-next-line no-restricted-properties
|
|
return it.only;
|
|
} else {
|
|
return it;
|
|
}
|
|
}
|
|
|
|
/** If required, returns a custom test timeout for long-running tests */
|
|
function getTestTimeout(tags: string[]): number | undefined {
|
|
if (tags.indexOf(BENCHMARK_TAG) >= 0) {
|
|
return BENCHMARK_TEST_TIMEOUT_MS;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Like it(), but for spec tests.
|
|
* @param name - A name to give the test.
|
|
* @param tags - Tags to apply to the test (e.g. 'exclusive' to only run
|
|
* individual tests)
|
|
* @param builder - A function that returns a spec.
|
|
* If writeToJSONFile has been called, the spec will be stored in
|
|
* `specsInThisTest`. Otherwise, it will be run, just as it() would run it.
|
|
*/
|
|
export function specTest(
|
|
name: string,
|
|
tags: string[],
|
|
builder: () => SpecBuilder
|
|
): void;
|
|
export function specTest(
|
|
name: string,
|
|
tags: string[],
|
|
comment: string,
|
|
builder: () => SpecBuilder
|
|
): void;
|
|
export function specTest(
|
|
name: string,
|
|
tags: string[],
|
|
commentOrBuilder: string | (() => SpecBuilder),
|
|
maybeBuilder?: () => SpecBuilder
|
|
): void {
|
|
let comment: string | undefined;
|
|
let builder: () => SpecBuilder;
|
|
if (typeof commentOrBuilder === 'string') {
|
|
comment = commentOrBuilder;
|
|
builder = maybeBuilder!;
|
|
} else {
|
|
builder = commentOrBuilder;
|
|
}
|
|
debugAssert(!!builder, 'Missing spec builder');
|
|
// Union in the tags for the describeSpec().
|
|
tags = tags.concat(describeTags);
|
|
for (const tag of tags) {
|
|
debugAssert(
|
|
KNOWN_TAGS.indexOf(tag) >= 0,
|
|
'Unknown tag "' + tag + '" on test: ' + name
|
|
);
|
|
}
|
|
|
|
if (!writeJSONFile) {
|
|
const persistenceModes = IndexedDbPersistence.isAvailable()
|
|
? [true, false]
|
|
: [false];
|
|
for (const usePersistence of persistenceModes) {
|
|
const convertToPipelines = [false, true];
|
|
for (const convertToPipeline of convertToPipelines) {
|
|
const runner = getTestRunner(tags, usePersistence, convertToPipeline);
|
|
const timeout = getTestTimeout(tags);
|
|
const mode = usePersistence ? '(Persistence)' : '(Memory)';
|
|
const queryMode = convertToPipeline ? '(Pipeline)' : '(Query)';
|
|
const fullName = `${mode} ${queryMode} ${name}`;
|
|
const queuedTest = runner(fullName, async () => {
|
|
const spec = builder();
|
|
const start = Date.now();
|
|
await spec.runAsTest(
|
|
fullName,
|
|
tags,
|
|
usePersistence,
|
|
convertToPipeline
|
|
);
|
|
const end = Date.now();
|
|
if (tags.indexOf(BENCHMARK_TAG) >= 0) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Runtime: ${end - start} ms.`);
|
|
}
|
|
});
|
|
|
|
if (timeout !== undefined) {
|
|
queuedTest.timeout(timeout);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
debugAssert(
|
|
tags.indexOf(EXCLUSIVE_TAG) === -1,
|
|
`The 'exclusive' tag is only supported for development and should not be exported to ` +
|
|
`other platforms.`
|
|
);
|
|
const spec = builder();
|
|
|
|
const specJSON = spec.toJSON();
|
|
|
|
const json = {
|
|
describeName,
|
|
itName: name,
|
|
tags,
|
|
comment,
|
|
config: specJSON.config,
|
|
steps: specJSON.steps
|
|
};
|
|
|
|
if (name in specsInThisTest) {
|
|
throw new Error('duplicate spec test: "' + name + '"');
|
|
}
|
|
specsInThisTest[name] = json;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Like describe, but for spec tests.
|
|
* @param name - A name to give the test.
|
|
* @param tags - Tags to apply to all tests in the spec (e.g. 'exclusive' to
|
|
* only run individual tests)
|
|
* @param builder - A function that calls specTest for each test case.
|
|
* If writeToJSONFile has been called, the specs will be stored in
|
|
* that file. Otherwise, they will be run, just as describe would run.
|
|
*/
|
|
export function describeSpec(
|
|
name: string,
|
|
tags: string[],
|
|
builder: () => void
|
|
): void {
|
|
describeTags = tags;
|
|
describeName = name;
|
|
|
|
if (!writeJSONFile) {
|
|
describe(name, () => {
|
|
addEqualityMatcher(
|
|
{ equalsFn: targetEquals, forType: TargetImpl },
|
|
{ equalsFn: queryEquals, forType: QueryImpl },
|
|
{ equalsFn: pipelineEq, forType: Pipeline }
|
|
);
|
|
return builder();
|
|
});
|
|
} else {
|
|
specsInThisTest = {};
|
|
builder();
|
|
// Note: We use json-stable-stringify instead of JSON.stringify() to ensure
|
|
// that the generated JSON does not produce diffs merely due to the order
|
|
// of the keys in an object changing.
|
|
const output = stringify(specsInThisTest, {
|
|
space: 2,
|
|
cmp: stringifyComparator
|
|
});
|
|
writeJSONFile(output || '');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The key ordering overrides used when sorting keys in the generated JSON.
|
|
* If both keys being compared are present in this array then they should be
|
|
* ordered in the generated JSON in the same relative order of this array.
|
|
*/
|
|
const stringifyCustomOrdering = [
|
|
'comment',
|
|
'describeName',
|
|
'itName',
|
|
'tags',
|
|
'config',
|
|
'steps'
|
|
];
|
|
|
|
/**
|
|
* Assigns a "group number" to the given key in the generated JSON.
|
|
*
|
|
* Keys with lower group numbers should be ordered before keys with greater
|
|
* group numbers. Keys with equal group numbers should be ordered
|
|
* alphabetically.
|
|
*/
|
|
function stringifyGroup(s: string): number {
|
|
const index = stringifyCustomOrdering.indexOf(s);
|
|
if (index >= 0) {
|
|
return index;
|
|
} else if (!s.startsWith('expected')) {
|
|
return stringifyCustomOrdering.length;
|
|
} else {
|
|
return stringifyCustomOrdering.length + 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A comparator function for stringify() that sorts keys in the JSON output.
|
|
*
|
|
* In order to produce JSON that has somewhat-intuitively-ordered keys, this
|
|
* comparator intentionally deviates from pure alphabetic ordering, placing
|
|
* some logically-first keys before others.
|
|
*/
|
|
function stringifyComparator(
|
|
a: { key: string; value: unknown },
|
|
b: { key: string; value: unknown }
|
|
): number {
|
|
const aGroup = stringifyGroup(a.key);
|
|
const bGroup = stringifyGroup(b.key);
|
|
if (aGroup === bGroup) {
|
|
return primitiveComparator(a.key, b.key);
|
|
} else {
|
|
return primitiveComparator(aGroup, bGroup);
|
|
}
|
|
}
|