Collections
Tests in this category ensure lists and maps cross the language boundary with correctly typed elements, and that returned collections are copies of the stored value.
An array stored in an instance property can be read
Test: arrayPropertyCanBeRead
When an array is passed to a constructor and stored in an instance property, the host MUST be able to read that property back from the kernel and observe the same elements, in order.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public map: { [key: string]: string };
public array: string[];
public constructor(map: { [key: string]: string }, array: string[]) {
this.map = map;
this.array = array;
}
}
// WHEN
const subject = new ClassWithCollections({}, ['one', 'two']);
// THEN
expect(subject.array).toEqual(['one', 'two']);
Arrays of object references cross the boundary preserving order and type
Test: arraysOfObjectsPreserveOrderAndType
The host MUST be able to assign an array of object references to a property and read it back from the kernel. The array MUST preserve the order of its elements, and each element MUST be returned as a reference to the same object that was sent, so that the host can invoke its members and observe the correct declared type.
Reference Implementation
// GIVEN
export abstract class NumericValue {
public abstract readonly value: number;
public abstract toString(): string;
}
export class Number extends NumericValue {
public constructor(public readonly value: number) {
super();
}
public toString() {
return `${this.value}`;
}
}
abstract class BinaryOperation extends NumericValue {
public constructor(public readonly lhs: NumericValue, public readonly rhs: NumericValue) {
super();
}
}
export class Add extends BinaryOperation {
public get value() {
return this.lhs.value + this.rhs.value;
}
public toString() {
return `(${this.lhs} + ${this.rhs})`;
}
}
export class Multiply extends BinaryOperation {
public get value() {
return this.lhs.value * this.rhs.value;
}
public toString() {
return `(${this.lhs} * ${this.rhs})`;
}
}
export class Sum extends NumericValue {
public parts: NumericValue[] = [];
public get expression(): NumericValue {
let curr: NumericValue = new Number(0);
for (const part of this.parts) {
curr = new Add(curr, part);
}
return curr;
}
public get value() {
return this.expression.value;
}
public toString() {
return this.expression.toString();
}
}
// WHEN
const sum = new Sum();
sum.parts = [new Number(5), new Number(10), new Multiply(new Number(2), new Number(3))];
// THEN
expect(sum.value).toBe(10 + 5 + 2 * 3);
expect(sum.parts[0].value).toBe(5);
expect(sum.parts[2].value).toBe(6);
expect(sum.toString()).toBe('(((0 + 5) + 10) + (2 * 3))');
Array- and map-typed properties can be set and read
Test: collectionPropertiesCanBeSetAndRead
The host MUST be able to assign to a property whose declared type is an array of primitives, and to a property whose declared type is a map of object references, and then read those values back from the kernel. The host MUST observe the elements it assigned, in order for the array, and by key for the map.
Reference Implementation
// GIVEN
export class Number {
public constructor(public readonly value: number) {}
}
export class AllTypes {
private arrayValue: string[] = [];
private mapValue: { [key: string]: Number } = {};
public get arrayProperty(): string[] {
return this.arrayValue;
}
public set arrayProperty(value: string[]) {
this.arrayValue = value;
}
public get mapProperty(): { [key: string]: Number } {
return this.mapValue;
}
public set mapProperty(value: { [key: string]: Number }) {
this.mapValue = value;
}
}
// WHEN
const types = new AllTypes();
types.arrayProperty = ['Hello', 'World'];
types.mapProperty = { Foo: new Number(123) };
// THEN
expect(types.arrayProperty[1]).toBe('World');
expect(types.mapProperty['Foo'].value).toBe(123);
Elements of a returned list of interfaces are usable through the interface
Test: listOfInterfacesElementsAreUsable
When a method returns a list whose declared element type is a behavioral interface, the host MUST receive each element typed as that interface and MUST be able to invoke the interface's members on it. Calls on an element MUST be dispatched across the boundary to its JavaScript implementation.
Reference Implementation
// GIVEN
export interface IBell {
ring(): void;
}
export class InterfaceCollections {
public static listOfInterfaces(): IBell[] {
return [
{
ring: () => {
return;
},
},
];
}
private constructor() {}
}
// WHEN
const items = InterfaceCollections.listOfInterfaces();
// THEN
expect(items).toHaveLength(1);
for (const item of items) {
// Each element is received typed as IBell, so its members can be invoked.
expect(() => item.ring()).not.toThrow();
}
Elements of a returned list of structs have the struct's apparent type
Test: listOfStructsElementsHaveStructType
When a method returns a list whose declared element type is a struct, the host MUST deserialize each element as a value of that struct type. The host MUST present every element with the struct's apparent type, so that the host's idiomatic type checks recognize it as that struct and the struct's properties are accessible. This matters for hosts that reify the element type of a list, where an element of the wrong type would be unusable.
Reference Implementation
// GIVEN
export interface StructA {
readonly requiredString: string;
readonly optionalString?: string;
readonly optionalNumber?: number;
}
export class InterfaceCollections {
public static listOfStructs(): StructA[] {
return [{ requiredString: "Hello, I'm String!" }];
}
private constructor() {}
}
// WHEN
const items = InterfaceCollections.listOfStructs();
// THEN
expect(items).toHaveLength(1);
for (const item of items) {
// Each element is received with the apparent type StructA, so its properties are accessible.
expect(item.requiredString).toBe("Hello, I'm String!");
}
Values of a returned map of interfaces are usable through the interface
Test: mapOfInterfacesValuesAreUsable
When a method returns a map (keyed by string) whose declared value type is a behavioral interface, the host MUST receive each value typed as that interface and MUST be able to invoke the interface's members on it. Calls on a value MUST be dispatched across the boundary to its JavaScript implementation.
Reference Implementation
// GIVEN
export interface IBell {
ring(): void;
}
export class InterfaceCollections {
public static mapOfInterfaces(): { [name: string]: IBell } {
return {
A: {
ring: () => {
return;
},
},
};
}
private constructor() {}
}
// WHEN
const items = InterfaceCollections.mapOfInterfaces();
// THEN
expect(Object.keys(items)).toHaveLength(1);
for (const item of Object.values(items)) {
// Each value is received typed as IBell, so its members can be invoked.
expect(() => item.ring()).not.toThrow();
}
Values of a returned map of structs have the struct's apparent type
Test: mapOfStructsValuesHaveStructType
When a method returns a map (keyed by string) whose declared value type is a struct, the host MUST deserialize each value as an instance of that struct type. The host MUST present every value with the struct's apparent type, so that the host's idiomatic type checks recognize it as that struct and the struct's properties are accessible. This matters for hosts that reify the value type of a map, where a value of the wrong type would be unusable.
Reference Implementation
// GIVEN
export interface StructA {
readonly requiredString: string;
readonly optionalString?: string;
readonly optionalNumber?: number;
}
export class InterfaceCollections {
public static mapOfStructs(): { [name: string]: StructA } {
return {
A: { requiredString: "Hello, I'm String!" },
};
}
private constructor() {}
}
// WHEN
const items = InterfaceCollections.mapOfStructs();
// THEN
expect(Object.keys(items)).toHaveLength(1);
for (const item of Object.values(items)) {
// Each value is received with the apparent type StructA, so its properties are accessible.
expect(item.requiredString).toBe("Hello, I'm String!");
}
A map stored in an instance property can be read
Test: mapPropertyCanBeRead
When a map is passed to a constructor and stored in an instance property, the host MUST be able to read that property back from the kernel and observe the same key/value pairs.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public map: { [key: string]: string };
public array: string[];
public constructor(map: { [key: string]: string }, array: string[]) {
this.map = map;
this.array = array;
}
}
// WHEN
const subject = new ClassWithCollections({ key: 'value' }, []);
// THEN
expect(subject.map).toEqual({ key: 'value' });
expect(Object.keys(subject.map)).toHaveLength(1);
A map read from an instance property rejects mutation
Test: mapPropertyRejectsMutation
A map the host reads from an instance property is a snapshot of the value in JavaScript, not a live view of it. The host MUST present such a returned map as read-only, so that attempting to add, remove, or replace entries is rejected rather than silently mutating a copy that JavaScript will never see.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public map: { [key: string]: string };
public array: string[];
public constructor(map: { [key: string]: string }, array: string[]) {
this.map = map;
this.array = array;
}
}
// WHEN
const subject = new ClassWithCollections({ key: 'value' }, []);
const map = subject.map;
// THEN
expect(() => {
(map as Readonly<Record<string, string>> as Record<string, string>)['keyTwo'] = 'valueTwo';
}).toThrow();
Maps of object references can be read from the kernel
Test: mapsOfObjectsCanBeRead
The host MUST be able to read a property or method result whose declared type is a map (keyed by string) whose values are arrays of object references. The host MUST observe every key present in the map, and MUST be able to read each nested array and the members of its elements.
Reference Implementation
// GIVEN
export abstract class NumericValue {
public abstract readonly value: number;
}
export class Number extends NumericValue {
public constructor(public readonly value: number) {
super();
}
}
export class Calculator {
public operationsMap: { [op: string]: NumericValue[] } = {};
private curr: NumericValue = new Number(0);
public add(value: number): void {
this.curr = new Number(this.curr.value + value);
this.record('add', this.curr);
}
public mul(value: number): void {
this.curr = new Number(this.curr.value * value);
this.record('mul', this.curr);
}
private record(op: string, result: NumericValue): void {
const list = (this.operationsMap[op] ??= []);
list.push(result);
}
}
// WHEN
const calc = new Calculator();
calc.add(10);
calc.add(20);
calc.mul(2);
// THEN
expect(calc.operationsMap['add'].length).toBe(2);
expect(calc.operationsMap['mul'].length).toBe(1);
expect(calc.operationsMap['add'][1].value).toBe(30);
An array returned by a method can be read
Test: returnedArrayCanBeRead
When a method returns an array, the host MUST be able to read its contents. The returned array MUST contain exactly the elements produced in JavaScript, in the same order.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public static createAList(): string[] {
return ['one', 'two'];
}
}
// WHEN
const list = ClassWithCollections.createAList();
// THEN
expect(list).toEqual(['one', 'two']);
An array returned by a method rejects mutation
Test: returnedArrayRejectsMutation
An array the host receives from the kernel is a snapshot of the value in JavaScript, not a live view of it. The host MUST present such a returned array as read-only, so that attempting to add, remove, or replace elements is rejected rather than silently mutating a copy that JavaScript will never see.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public static createAList(): string[] {
return ['one', 'two'];
}
}
// WHEN
const list = ClassWithCollections.createAList();
// THEN
expect(() => (list as readonly string[] as string[]).push('three')).toThrow();
A map returned by a method can be read
Test: returnedMapCanBeRead
When a method returns a map (keyed by string), the host MUST be able to read its contents. The returned map MUST contain exactly the key/value pairs produced in JavaScript.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public static createAMap(): { [key: string]: string } {
return { key1: 'value1', key2: 'value2' };
}
}
// WHEN
const map = ClassWithCollections.createAMap();
// THEN
expect(map).toEqual({ key1: 'value1', key2: 'value2' });
expect(Object.keys(map)).toHaveLength(2);
A map returned by a method rejects mutation
Test: returnedMapRejectsMutation
A map the host receives from the kernel is a snapshot of the value in JavaScript, not a live view of it. The host MUST present such a returned map as read-only, so that attempting to add, remove, or replace entries is rejected rather than silently mutating a copy that JavaScript will never see.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public static createAMap(): { [key: string]: string } {
return { key1: 'value1', key2: 'value2' };
}
}
// WHEN
const map = ClassWithCollections.createAMap();
// THEN
expect(() => {
(map as Readonly<Record<string, string>> as Record<string, string>)['keyThree'] = 'valueThree';
}).toThrow();
A static array property can be read
Test: staticArrayPropertyCanBeRead
The host MUST be able to read a static property whose declared type is an array, without creating an instance of the class. The returned array MUST contain exactly the elements initialized in JavaScript, in order.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public static staticArray: string[] = ['one', 'two'];
}
// WHEN
const list = ClassWithCollections.staticArray;
// THEN
expect(list).toEqual(['one', 'two']);
A static array property rejects mutation
Test: staticArrayPropertyRejectsMutation
An array the host reads from a static property is a snapshot of the value in JavaScript, not a live view of it. The host MUST present such a returned array as read-only, so that attempting to add, remove, or replace elements is rejected rather than silently mutating a copy that JavaScript will never see.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public static staticArray: string[] = ['one', 'two'];
}
// WHEN
const list = ClassWithCollections.staticArray;
// THEN
expect(() => (list as readonly string[] as string[]).push('three')).toThrow();
A static map property can be read
Test: staticMapPropertyCanBeRead
The host MUST be able to read a static property whose declared type is a map (keyed by string), without creating an instance of the class. The returned map MUST contain exactly the key/value pairs initialized in JavaScript.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public static staticMap: { [key: string]: string } = {
key1: 'value1',
key2: 'value2',
};
}
// WHEN
const map = ClassWithCollections.staticMap;
// THEN
expect(map).toEqual({ key1: 'value1', key2: 'value2' });
expect(Object.keys(map)).toHaveLength(2);
A static map property rejects mutation
Test: staticMapPropertyRejectsMutation
A map the host reads from a static property is a snapshot of the value in JavaScript, not a live view of it. The host MUST present such a returned map as read-only, so that attempting to add, remove, or replace entries is rejected rather than silently mutating a copy that JavaScript will never see.
Reference Implementation
// GIVEN
export class ClassWithCollections {
public static staticMap: { [key: string]: string } = {
key1: 'value1',
key2: 'value2',
};
}
// WHEN
const map = ClassWithCollections.staticMap;
// THEN
expect(() => {
(map as Readonly<Record<string, string>> as Record<string, string>)['keyTwo'] = 'valueTwo';
}).toThrow();