Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(shared-tree): Add simple LazyOptionalField unit tests #17475

Merged
merged 41 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b008faa
fix: Bad comparison
Josmithr Sep 20, 2023
9ca7865
test: [WIP] LazyField unit tests
Josmithr Sep 20, 2023
b367f96
Merge branch 'main' into shared-tree/lazy-field-tests
Josmithr Sep 20, 2023
aa81a10
test: Expand test
Josmithr Sep 20, 2023
fd0dd1f
docs: Fix link
Josmithr Sep 20, 2023
c08f500
docs: Fix link
Josmithr Sep 20, 2023
3345626
test: [WIP] More test coverage
Josmithr Sep 20, 2023
1e0f593
WIP
Josmithr Sep 21, 2023
447f609
Merge branch 'main' into shared-tree/lazy-field-tests
Josmithr Sep 21, 2023
f658f80
test: Fix tests
Josmithr Sep 22, 2023
9dcbfb0
test: Add more tests
Josmithr Sep 22, 2023
38bd15f
Merge branch 'main' into shared-tree/lazy-field-tests
Josmithr Sep 25, 2023
5bdfd0f
test: Simplify infra
Josmithr Sep 25, 2023
becf213
test: Expand test cases
Josmithr Sep 25, 2023
0d8b427
test: Missing value cases
Josmithr Sep 26, 2023
3a8214c
test: Struct cases
Josmithr Sep 26, 2023
feae73c
test: Restore all tests
Josmithr Sep 26, 2023
0653e71
Merge branch 'main' into shared-tree/lazy-field-tests
Josmithr Sep 26, 2023
27b4890
fix: Type name
Josmithr Sep 26, 2023
b023e0f
docs: Add comment
Josmithr Sep 26, 2023
e09bf98
Merge branch 'main' into shared-tree/lazy-field-tests
Josmithr Sep 26, 2023
36b0d5c
test: Add cursor helper
Josmithr Sep 26, 2023
ead1b83
refactor: Share `contextWithContentReadonly`
Josmithr Sep 26, 2023
a3fbbe2
refactor: Use `TypedSchemaCollection`
Josmithr Sep 26, 2023
fe0b6be
refactor: Rename helper
Josmithr Sep 26, 2023
9b1852d
test: Better recursive case tests
Josmithr Sep 26, 2023
4dc53c6
test: Rename cases
Josmithr Sep 26, 2023
2c45f48
refactor: Rename helper
Josmithr Sep 26, 2023
97f381e
test: Remove unneeded cases
Josmithr Sep 26, 2023
243f7b4
Merge branch 'main' into shared-tree/lazy-field-tests
Josmithr Sep 26, 2023
8c697a5
Merge branch 'main' into shared-tree/lazy-field-tests
Josmithr Sep 26, 2023
27600c7
test: Re-remove `only`
Josmithr Sep 26, 2023
34c3b4e
docs: Better comments
Josmithr Sep 27, 2023
20c7aac
docs: Better comment
Josmithr Sep 27, 2023
7e17a8e
refactor: Simplify test utils setup
Josmithr Sep 27, 2023
cf6dbd5
test: Restore Fluid Handle tests
Josmithr Sep 27, 2023
c8f50d8
test: Remove Fluid Handle tests, for now
Josmithr Sep 27, 2023
79d80ca
docs: Add TODOs
Josmithr Sep 27, 2023
9ab8d68
test: Add struct field `is` test
Josmithr Sep 27, 2023
537307f
Merge branch 'main' into shared-tree/lazy-field-tests
Josmithr Sep 27, 2023
37ac7b5
test: Fix test name
Josmithr Sep 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -130,7 +130,7 @@ export interface TreeNode extends Tree<TreeSchema> {
* 1. To hold the children of non-leaf {@link TreeNode}s.
* 2. As the root of a {@link Tree}.
*
* Down-casting (via {@link TreeField#is}) is required to access Schema-Aware APIs, including editing.
Josmithr marked this conversation as resolved.
Show resolved Hide resolved
* Down-casting (via {@link TreeField.is}) is required to access Schema-Aware APIs, including editing.
* All content in the tree is accessible without down-casting, but if the schema is known,
* the schema aware API may be more ergonomic.
*
Expand Down
Expand Up @@ -313,7 +313,7 @@ export class FieldSchema<Kind extends FieldKindTypes = FieldKindTypes, Types = A
protected _typeCheck?: MakeNominal;

/**
* @param kind - The [kind](https://en.wikipedia.org/wiki/Kind_(type_theory)) of this field.
* @param kind - The {@link https://en.wikipedia.org/wiki/Kind_(type_theory) | kind} of this field.
* Determine the multiplicity, viewing and editing APIs as well as the merge resolution policy.
* @param allowedTypes - What types of tree nodes are allowed in this field.
*/
Expand Down
Expand Up @@ -3,24 +3,51 @@
* Licensed under the MIT License.
*/

/* eslint-disable import/no-internal-modules */

import { strict as assert } from "assert";

import { Any, SchemaBuilder } from "../../../feature-libraries";
import { FieldKey, TreeNavigationResult } from "../../../core";
// import { IFluidHandle } from "@fluidframework/core-interfaces";
// import { MockHandle } from "@fluidframework/test-runtime-utils";

import {
type AllowedTypes,
Any,
type FieldKindTypes,
FieldKinds,
FieldSchema,
SchemaAware,
SchemaBuilder,
TreeSchema,
} from "../../../feature-libraries";
import {
FieldAnchor,
FieldKey,
type IEditableForest,
type ITreeCursorSynchronous,
type ITreeSubscriptionCursor,
rootFieldKey,
TreeNavigationResult,
ValueSchema,
} from "../../../core";
import { forestWithContent } from "../../utils";
import { brand } from "../../../util";
import { type Context } from "../../../feature-libraries/editable-tree-2/context";
import {
LazyOptionalField,
LazySequence,
LazyValueField,
// eslint-disable-next-line import/no-internal-modules
} from "../../../feature-libraries/editable-tree-2/lazyField";
import { getReadonlyContext } from "./utils";

const detachedField: FieldKey = brand("detached");
const detachedFieldAnchor = { parent: undefined, fieldKey: detachedField };
const detachedFieldAnchor: FieldAnchor = { parent: undefined, fieldKey: detachedField };
const rootFieldAnchor: FieldAnchor = { parent: undefined, fieldKey: rootFieldKey };

// Mocks the ID representing a Fluid handle for test-purposes.
// const mockFluidHandle = new MockHandle(5) as IFluidHandle;

describe("lazyField", () => {
describe("LazyField", () => {
it("LazyField implementations do not allow edits to detached trees", () => {
const builder = new SchemaBuilder("lazyTree");
builder.struct("empty", {});
Expand All @@ -29,7 +56,7 @@ describe("lazyField", () => {
const context = getReadonlyContext(forest, schema);
const cursor = context.forest.allocateCursor();
assert.equal(
forest.tryMoveCursorToField({ fieldKey: detachedField, parent: undefined }, cursor),
forest.tryMoveCursorToField(detachedFieldAnchor, cursor),
TreeNavigationResult.Ok,
);
const sequenceField = new LazySequence(
Expand Down Expand Up @@ -65,3 +92,314 @@ describe("lazyField", () => {
);
});
});

function createSingleValueTree<Kind extends FieldKindTypes, Types extends AllowedTypes>(
builder: SchemaBuilder,
Josmithr marked this conversation as resolved.
Show resolved Hide resolved
rootSchema: FieldSchema<Kind, Types>,
initialTree?:
| SchemaAware.TypedField<FieldSchema, SchemaAware.ApiMode.Flexible>
| readonly ITreeCursorSynchronous[]
| ITreeCursorSynchronous,
): {
context: Context;
cursor: ITreeSubscriptionCursor;
forest: IEditableForest;
} {
const schema = builder.intoDocumentSchema(rootSchema);
const forest = forestWithContent({ schema, initialTree });

const context = getReadonlyContext(forest, schema);
const cursor = context.forest.allocateCursor();

assert.equal(
context.forest.tryMoveCursorToField(rootFieldAnchor, cursor),
TreeNavigationResult.Ok,
);

return {
forest,
context,
cursor,
};
}

// TODO: no only
describe.only("LazyOptionalField", () => {
describe("as", () => {
Josmithr marked this conversation as resolved.
Show resolved Hide resolved
it("Any", () => {
const builder = new SchemaBuilder("test");
const booleanLeafSchema = builder.leaf("bool", ValueSchema.Boolean);

const { context, cursor } = createSingleValueTree(
builder,
SchemaBuilder.fieldOptional(builder.struct("struct", {})),
{},
);

const field = new LazyOptionalField(
context,
SchemaBuilder.fieldOptional(Any),
cursor,
detachedFieldAnchor,
);

// Positive cases
assert(field.is(SchemaBuilder.fieldOptional(Any)));
assert(field.is(SchemaBuilder.fieldRecursive(FieldKinds.optional, Any)));

// Negative cases
assert(!field.is(SchemaBuilder.fieldOptional()));
assert(!field.is(SchemaBuilder.fieldOptional(booleanLeafSchema)));
assert(!field.is(SchemaBuilder.fieldValue(Any)));
assert(!field.is(SchemaBuilder.fieldSequence(Any)));
assert(!field.is(SchemaBuilder.fieldRecursive(FieldKinds.optional, booleanLeafSchema)));
Josmithr marked this conversation as resolved.
Show resolved Hide resolved
});

it("Boolean", () => {
const builder = new SchemaBuilder("test");
const booleanLeafSchema = builder.leaf("bool", ValueSchema.Boolean);
const numberLeafSchema = builder.leaf("number", ValueSchema.Number);

const { context, cursor } = createSingleValueTree(
builder,
SchemaBuilder.fieldOptional(builder.struct("struct", {})),
{},
);

assert.equal(
context.forest.tryMoveCursorToField(detachedFieldAnchor, cursor),
TreeNavigationResult.Ok,
);

const field = new LazyOptionalField(
context,
SchemaBuilder.fieldOptional(booleanLeafSchema),
cursor,
detachedFieldAnchor,
);

// Positive cases
assert(field.is(SchemaBuilder.fieldOptional(booleanLeafSchema)));
assert(field.is(SchemaBuilder.fieldRecursive(FieldKinds.optional, booleanLeafSchema)));

// Negative cases
assert.equal(field.is(SchemaBuilder.fieldValue(Any)), false);
assert.equal(field.is(SchemaBuilder.fieldValue(booleanLeafSchema)), false);
assert.equal(field.is(SchemaBuilder.fieldValue(numberLeafSchema)), false);
assert.equal(field.is(SchemaBuilder.fieldSequence(Any)), false);
assert.equal(field.is(SchemaBuilder.fieldSequence(booleanLeafSchema)), false);
assert.equal(field.is(SchemaBuilder.fieldSequence(numberLeafSchema)), false);
// assert.equal(
Josmithr marked this conversation as resolved.
Show resolved Hide resolved
// field.is(SchemaBuilder.fieldRecursive(FieldKinds.optional, numberLeafSchema)),
// false,
// );
});

// TODO: what other cases are interesting?
});

describe("length", () => {
it("No value", () => {
const builder = new SchemaBuilder("test");
const numberLeafSchema = builder.leaf("number", ValueSchema.Number);
const rootSchema = SchemaBuilder.fieldOptional(numberLeafSchema);

const { context, cursor } = createSingleValueTree(builder, rootSchema, undefined);

const field = new LazyOptionalField(
context,
SchemaBuilder.fieldOptional(Any),
cursor,
rootFieldAnchor,
);

assert.equal(field.length, 0);
});

it("With value", () => {
const builder = new SchemaBuilder("test");
const numberLeafSchema = builder.leaf("number", ValueSchema.Number);
const rootSchema = SchemaBuilder.fieldOptional(numberLeafSchema);

const { context, cursor } = createSingleValueTree(builder, rootSchema, 42);

const field = new LazyOptionalField(
context,
SchemaBuilder.fieldOptional(numberLeafSchema),
cursor,
rootFieldAnchor,
);

assert.equal(field.length, 1);
});
});

/**
* Creates a single-node, primitive tree, and returns a field associated with that node.
*/
function createPrimitiveField(
Josmithr marked this conversation as resolved.
Show resolved Hide resolved
kind: ValueSchema,
initialTree?:
| SchemaAware.TypedField<FieldSchema, SchemaAware.ApiMode.Flexible>
| readonly ITreeCursorSynchronous[]
| ITreeCursorSynchronous,
): LazyOptionalField<[TreeSchema<"leaf">]> {
const builder = new SchemaBuilder("test");
const leafSchema = builder.leaf("leaf", kind);
const rootSchema = SchemaBuilder.fieldOptional(leafSchema);

const { context, cursor } = createSingleValueTree(builder, rootSchema, initialTree);

return new LazyOptionalField(
context,
SchemaBuilder.fieldOptional(leafSchema),
cursor,
rootFieldAnchor,
);
}

function createStructField(
initialTree?:
| SchemaAware.TypedField<FieldSchema, SchemaAware.ApiMode.Flexible>
| readonly ITreeCursorSynchronous[]
| ITreeCursorSynchronous,
): LazyOptionalField<[TreeSchema<"struct">]> {
const builder = new SchemaBuilder("test");
const booleanLeafSchema = builder.leaf("bool", ValueSchema.Boolean);
const numberLeafSchema = builder.leaf("number", ValueSchema.Number);
const leafSchema = builder.struct("struct", {
foo: SchemaBuilder.fieldValue(booleanLeafSchema),
bar: SchemaBuilder.fieldOptional(numberLeafSchema),
});
const rootSchema = SchemaBuilder.fieldOptional(leafSchema);

const { context, cursor } = createSingleValueTree(builder, rootSchema, initialTree);

return new LazyOptionalField(
context,
SchemaBuilder.fieldOptional(leafSchema),
cursor,
rootFieldAnchor,
);
}

describe("map", () => {
it("boolean", () => {
const field = createPrimitiveField(ValueSchema.Boolean, false);

assert.deepEqual(
field.map((value) => value),
[false],
);
});

it("number", () => {
const field = createPrimitiveField(ValueSchema.Number, 42);

assert.deepEqual(
field.map((value) => value),
[42],
);
});

it("string", () => {
const field = createPrimitiveField(ValueSchema.String, "Hello world");

assert.deepEqual(
field.map((value) => value),
["Hello world"],
);
});

// TODO: current types don't allow fluid handle
// it("Fluid Handle", () => {
// const field = createPrimitiveField(ValueSchema.FluidHandle, mockFluidHandle);
Josmithr marked this conversation as resolved.
Show resolved Hide resolved

// assert.deepEqual(
// field.map((value) => value),
// [mockFluidHandle],
// );
// });

it("No value", () => {
const field = createPrimitiveField(ValueSchema.Number, undefined);

assert.deepEqual(
field.map((value) => value),
[],
);
});

it("Non-primitive field", () => {
const input = {
foo: true,
bar: 42,
};
const field = createStructField(input);

const mapResult = field.map((value) => value);

assert.equal(mapResult.length, 1);
assert.notEqual(mapResult[0], undefined);
assert.equal((mapResult[0] as any).foo, true);
assert.equal((mapResult[0] as any).bar, 42);
});
});

describe("mapBoxed", () => {
it("number", () => {
const field = createPrimitiveField(ValueSchema.Number, 42);

const mapResult = field.mapBoxed((value) => value);
assert.equal(mapResult.length, 1);
assert.equal(mapResult[0].value, 42);
});

it("boolean", () => {
const field = createPrimitiveField(ValueSchema.Boolean, true);

const mapResult = field.mapBoxed((value) => value);
assert.equal(mapResult.length, 1);
assert.equal(mapResult[0].value, true);
});

it("string", () => {
const field = createPrimitiveField(ValueSchema.String, "Hello world");

const mapResult = field.mapBoxed((value) => value);
assert.equal(mapResult.length, 1);
assert.equal(mapResult[0].value, "Hello world");
});

// TODO: current types don't allow fluid handle
// it("Fluid Handle", () => {
// const field = createPrimitiveField(ValueSchema.FluidHandle, mockFluidHandle);

// const mapResult = field.mapBoxed((value) => value);
// assert.equal(mapResult.length, 1);
// assert.equal(mapResult[0].value, mockFluidHandle);
// });

it("No value", () => {
const field = createPrimitiveField(ValueSchema.String, undefined);

const mapResult = field.mapBoxed((value) => value);
assert.deepEqual(mapResult, []);
});

it("Non-primitive field", () => {
const input = {
foo: true,
bar: 42,
};
const field = createStructField(input);

const mapResult = field.mapBoxed((value) => value);

assert.equal(mapResult.length, 1);
assert.notEqual(mapResult[0], undefined);
assert.equal((mapResult[0] as any).foo, input.foo);
assert.equal((mapResult[0] as any).bar, input.bar);
});
});
});