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

Refactor: engine <-> client interface implementation #67

Merged
merged 87 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
daafd95
DX improvement: ESLint recognizes JSDoc type references
eyelidlessness Mar 18, 2024
526d084
Update existing READMEs to reflect changes to GitHub’s special note b…
eyelidlessness Mar 12, 2024
3345ca8
Initial engine/client interface: types, explainer README, doc gen script
eyelidlessness Mar 12, 2024
538e37b
Expand client interface resource types
eyelidlessness Mar 12, 2024
bf2c2de
Add compatibility for reading text from Blob values
eyelidlessness Mar 12, 2024
1f13ae4
Engine-client implementation: (XML) resource retrieval
eyelidlessness Mar 12, 2024
5d0798b
Minimal internal reactivity implementation, sutiable for testing…
eyelidlessness Mar 16, 2024
0f7c0b7
Minor client API refinements: select options, relax opaque reactive o…
eyelidlessness Mar 18, 2024
74ad508
Initial structure of engine implementation of client interface
eyelidlessness Mar 28, 2024
e000288
All nodes get a unique (per session at least) id
eyelidlessness Mar 20, 2024
5c88575
Initial support for engine/client shared state
eyelidlessness Mar 28, 2024
5ae3bda
Implement most `Root`/`RootNode` functionality
eyelidlessness Mar 28, 2024
fd8eace
First pass making all node implementations non-abstract
eyelidlessness Mar 28, 2024
ca7e9a3
First pass, build the initial instance tree
eyelidlessness Mar 28, 2024
87d2352
Expose new client interface/implementation …
eyelidlessness Mar 28, 2024
3062bc5
WIP: Update ui-solid to use new client interface
eyelidlessness Mar 28, 2024
66f5340
Fix: ui-solid/xforms-engine reactivity propagation
eyelidlessness Mar 28, 2024
eda41a7
(Temporarily?) ui-solid uses xforms-engine source
eyelidlessness Mar 22, 2024
d83435e
Introduce general concept of shared state initialization
eyelidlessness Mar 28, 2024
0b89575
Generalized API for reactive `DependentExpression`s
eyelidlessness Mar 29, 2024
60411c5
Bind computations: `readonly`, `relevant`, `required`
eyelidlessness Mar 29, 2024
432512c
Initial implementation of value state, StringField value
eyelidlessness Mar 29, 2024
355f232
Prevent writes to readonly fields
eyelidlessness Mar 31, 2024
a856c6d
Set non-relevant value state to blank…
eyelidlessness Mar 31, 2024
28f2e01
Implement general solution for reactive text, labels, hints
eyelidlessness Mar 29, 2024
e0200c1
Wire up reactive Group labels, StringField labels/hints
eyelidlessness Mar 29, 2024
752aaf2
Initial implementation of select items
eyelidlessness Mar 29, 2024
107f35c
Initial support for SelectField value writes
eyelidlessness Mar 29, 2024
7c802be
Initial SelectField `subscribe` implementation
eyelidlessness Mar 29, 2024
86362d5
Support for adding and removing repeat instances
eyelidlessness Mar 30, 2024
3690e36
Provide label state for repeat instancres
eyelidlessness Mar 31, 2024
d308b58
Make the new instance state implementation reactive!
eyelidlessness Mar 31, 2024
7d7ef17
Where we’re going, we don’t need (manual) toposort!
eyelidlessness Mar 31, 2024
a98dc4e
CI side quest: ui-solid browser test tree-sitter-xpath failure
eyelidlessness Apr 1, 2024
6b83201
Eliminate Solid client state special case…
eyelidlessness Apr 2, 2024
8b46fcd
Map `children` state by node ID
eyelidlessness Apr 2, 2024
f174f61
Client API cleanup: remove unused type from hierarchy
eyelidlessness Apr 3, 2024
89580e3
Prepare to migrate EntryState tests to new client interface
eyelidlessness Apr 3, 2024
0429f95
Fix: `childrenState` must be defined before other reactive node state…
eyelidlessness Apr 3, 2024
c92188c
Fix: reactivity of label/hint outputs and translations
eyelidlessness Apr 3, 2024
9e04a53
Fix: client interface `StringNode` specify its `currentState` type!
eyelidlessness Apr 3, 2024
fbd70f2
Create scenario package for ported JavaRosa tests
eyelidlessness Apr 4, 2024
0d3cae8
Migrate `Scenario` to new client interface, minor test updates for async
eyelidlessness Apr 4, 2024
6b8330b
This `createRoot` is now superfluous
eyelidlessness Apr 4, 2024
39e5355
Add passing relative version of currently failing repeat test
eyelidlessness Apr 4, 2024
c212529
Update ui-solid translations test to new client interface
eyelidlessness Apr 4, 2024
edf4ebc
Update subset of ui-solid XFormDetails component…
eyelidlessness Apr 4, 2024
5d2c030
Remove xforms-engine exports of `EntryState` APIs…
eyelidlessness Apr 4, 2024
0dfd1be
Relax internal reactivity types
eyelidlessness Apr 4, 2024
48d12a5
Prepare to migrate remaining EntryState tests to new instance impleme…
eyelidlessness Apr 4, 2024
25eedb5
Migrate remaining EntryState tests to new client API
eyelidlessness Apr 4, 2024
8b9b45c
Mark intentional failure for change to recalculate re-relevant…
eyelidlessness Apr 4, 2024
421ffd9
Remove final remnants of `EntryState`
eyelidlessness Apr 4, 2024
4c13a87
Remove reactive libs no longer in use
eyelidlessness Apr 4, 2024
6c0f03b
Remove dependencies no longer in use
eyelidlessness Apr 4, 2024
6433974
Fix: don’t try to call fake XPath expression for static text in labels
eyelidlessness Apr 5, 2024
0a4677a
Fix: include label on `RepeatRangeNode`/`RepeatRange`
eyelidlessness Apr 5, 2024
d38b509
ui-solid: render label of repeat range when repeat instance has no la…
eyelidlessness Apr 5, 2024
0d4931f
Expose `AnyLeafNode` in client interface …
eyelidlessness Apr 5, 2024
56246c5
Move xforms-engine test files out of src …
eyelidlessness Apr 9, 2024
a043f42
Remove `engineConfig` from client `*Node` interfaces
eyelidlessness Apr 9, 2024
f2fe779
Remove @todo on client interface `activeLanguage` JSDoc…
eyelidlessness Apr 9, 2024
a6e2e5f
Add JSDoc example for `SubtreeNode`
eyelidlessness Apr 9, 2024
3f4cb19
Remove apparently-unnecessary try/catch in `createUniqueId`
eyelidlessness Apr 9, 2024
4f7c37d
Eliminate unnecessary type indirection for `getInstanceConfig`
eyelidlessness Apr 9, 2024
6f05051
Narrow `initializeForm` type to client interface in build…
eyelidlessness Apr 9, 2024
a2ce805
Trim string input to `retrieveSourceXMLResource`
eyelidlessness Apr 9, 2024
caa4b44
Fix: assignability of Vue’s `reactive` to factory type
eyelidlessness Apr 10, 2024
9625f7b
Improve readability of inserting `RepeatInstance` in `addInstances`
eyelidlessness Apr 10, 2024
202f220
Make InstanceNode.engineConfig a constructor assignment
eyelidlessness Apr 10, 2024
2b51442
Add JSDoc and `@package` access scope to `InstanceNode.getChildren`.
eyelidlessness Apr 10, 2024
b85a254
`nodeType` (all nodes) and `nodeVariant` (node-specific)…
eyelidlessness Apr 10, 2024
5eab1fe
Simplify `Scenario` type refinements by narrowing with `nodeType`
eyelidlessness Apr 10, 2024
8ef9bef
Simplify ui-solid type refinements, prepare for further improvement…
eyelidlessness Apr 10, 2024
3f85bec
Throw when children state and their NodeID[] representations diverge
eyelidlessness Apr 10, 2024
9657164
Simplify compute -> set `RepeatInstance` index state
eyelidlessness Apr 10, 2024
d3a60a6
Move constant expression memo into reactive scope task
eyelidlessness Apr 10, 2024
fb13e03
Call value setter with object rather than callback
eyelidlessness Apr 10, 2024
95641c5
Roll back addition of `nodeVariant` property for now
eyelidlessness Apr 11, 2024
849ae5d
Remove wrong (and typo) condition around `currentState` write failure…
eyelidlessness Apr 11, 2024
725a74e
Move remaining .test.ts(x) files src -> test (parallel FS structure)
eyelidlessness Apr 11, 2024
a687791
Remove unnecessary optional access on recursive call to internal `get…
eyelidlessness Apr 11, 2024
7a465bd
Repeat instance removal of `contextNode` is special case
eyelidlessness Apr 11, 2024
51f1a05
Remove `InstanceNodeState` and `DescendantNodeState`
eyelidlessness Apr 11, 2024
9a4b937
Generalize `insertAtIndex` as `common` package lib
eyelidlessness Apr 11, 2024
a84c386
Generalize `identity` function as `common` package lib
eyelidlessness Apr 11, 2024
81eeae1
Use`identity` lib function for `StringField` encodeValue/decodeValue …
eyelidlessness Apr 11, 2024
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
69 changes: 69 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
tree-sitter-xpath: ${{ steps.changes.outputs.tree-sitter-xpath }}
xforms-engine: ${{ steps.changes.outputs.xforms-engine }}
xpath: ${{ steps.changes.outputs.xpath }}
scenario: ${{ steps.changes.outputs.scenario }}
ui-solid: ${{ steps.changes.outputs.ui-solid }}
ui-vue: ${{ steps.changes.outputs.ui-vue }}

Expand All @@ -57,6 +58,9 @@ jobs:
xforms-engine:
- 'packages/xforms-engine/**'

scenario:
- 'packages/scenario/**'

xpath:
- 'packages/xpath/**'

Expand Down Expand Up @@ -295,6 +299,71 @@ jobs:
- if: ${{ matrix.target == 'Web' }}
run: 'yarn workspace @odk-web-forms/xforms-engine test-browser:${{ matrix.browser }}'

scenario:
name: 'scenario'
needs: ['install-and-build', 'changes']
if: needs.changes.outputs.root == 'true' || needs.changes.outputs.scenario == 'true'
runs-on: 'ubuntu-latest'

strategy:
matrix:
target: ['Node']
node-version: ['18.19.1', '20.11.1']
include:
- target: 'Web'
node-version: '20.11.1'
browser: chromium
- target: 'Web'
node-version: '20.11.1'
browser: firefox
- target: 'Web'
node-version: '20.11.1'
browser: webkit

steps:
- uses: 'actions/checkout@v3'

- uses: 'volta-cli/action@v4'
with:
node-version: '${{ matrix.node-version }}'
yarn-version: '1.22.19'

- uses: 'actions/cache@v3'
id: cache-install
with:
path: |
node_modules
**/node_modules
key: install-${{ matrix.node-version }}-${{ hashFiles('yarn.lock', 'examples/*/yarn.lock', 'packages/*/yarn.lock') }}
fail-on-cache-miss: true

- uses: 'actions/cache@v3'
id: cache-build
with:
path: |
examples/*/dist
packages/*/dist
packages/tree-sitter-xpath/grammar.js
packages/tree-sitter-xpath/src/grammar.json
packages/tree-sitter-xpath/src/parser.c
packages/tree-sitter-xpath/src/tree_sitter/parser.h
packages/tree-sitter-xpath/tree-sitter-xpath.wasm
packages/tree-sitter-xpath/types
key: build-${{ matrix.node-version }}-${{ github.sha }}
fail-on-cache-miss: true

- if: ${{ matrix.target == 'Node' }}
run: 'yarn workspace @odk-web-forms/scenario test:types'

- if: ${{ matrix.target == 'Node' }}
run: 'yarn workspace @odk-web-forms/scenario test-node:jsdom'

- if: ${{ matrix.target == 'Web' }}
run: 'yarn playwright install ${{ matrix.browser }} --with-deps'

- if: ${{ matrix.target == 'Web' }}
run: 'yarn workspace @odk-web-forms/scenario test-browser:${{ matrix.browser }}'

xpath:
name: '@odk-web-forms/xpath'
needs: ['install-and-build', 'changes']
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ dist/
examples/**/dist
packages/**/dist

# Generated documentation
packages/*/api-docs

## Specific to tree-sitter and/or @odk-web-forms/tree-sitter-xpath
packages/tree-sitter-xpath/bindings/
packages/tree-sitter-xpath/build/
Expand Down
14 changes: 14 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import { FlatCompat } from '@eslint/eslintrc';
import eslint from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import jsdoc from 'eslint-plugin-jsdoc';
import noOnlyTestsPlugin from 'eslint-plugin-no-only-tests';
import vuePlugin from 'eslint-plugin-vue';
import vueBase from 'eslint-plugin-vue/lib/configs/base.js';
Expand Down Expand Up @@ -104,6 +105,19 @@ export default tseslint.config(
],
},

{
plugins: { jsdoc },
rules: {
'jsdoc/no-undefined-types': [
'error',
{
markVariablesAsUsed: true,
disableReporting: true,
},
],
},
},

{
extends: [
eslint.configs.recommended,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsdoc": "^48.2.1",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.22.0",
Expand Down
23 changes: 23 additions & 0 deletions packages/common/src/lib/array/insert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const insertAtIndex = <T>(
currentValues: readonly T[],
insertionIndex: number,
newValue: T
): readonly T[] => {
const { length } = currentValues;

if (insertionIndex > length) {
throw new Error(
'Failed to insert value: specified insertion index is greater than the current array length, which would introduce empty array slots'
);
}

if (insertionIndex === length) {
return currentValues.concat(newValue);
}

return [
...currentValues.slice(0, insertionIndex),
newValue,
...currentValues.slice(insertionIndex),
];
};
1 change: 1 addition & 0 deletions packages/common/src/lib/identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const identity = <T>(value: T): T => value;
23 changes: 23 additions & 0 deletions packages/common/src/lib/objects/structure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type PropertyKeys<T> = ReadonlyArray<string & keyof T>;

export const getPropertyKeys = <T extends object>(object: T): PropertyKeys<T> => {
return Object.keys(object) as Array<string & keyof T>;
};

export type PropertyDescriptorEntry<T> = readonly [string & keyof T, PropertyDescriptor];

export type PropertyDescriptors<T> = Array<PropertyDescriptorEntry<T>>;

export const getPropertyDescriptors = <T extends object>(object: T): PropertyDescriptors<T> => {
const keys = getPropertyKeys(object);

return keys.map((key) => {
const descriptor = Object.getOwnPropertyDescriptor(object, key);

if (descriptor == null) {
throw new Error(`Could not get property descriptor for key ${key}`);
}

return [key, descriptor];
});
};
60 changes: 60 additions & 0 deletions packages/common/src/lib/web-compat/blob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Gets the text content of a {@link Blob} (or {@link File}).
*
* Why does this exist when there's a standard way to get the text of a `Blob`
* (or `File`)? Because
* {@link https:/jsdom/jsdom/issues/2555 | jsdom} considers it out
* of spec... to implement the spec.
*/
let getBlobText: (blob: Blob) => Promise<string>;

const isBlobBrokenByDesign = async (): Promise<boolean> => {
try {
const blob = new Blob(['a']);
const text = await blob.text();

return text !== 'a';
} catch {
return true;
}
};

if (await isBlobBrokenByDesign()) {
getBlobText = (blob) => {
return new Promise((resolve, reject) => {
let isDone = false;

const reader = new FileReader();

const complete = () => {
if (isDone) {
throw new Error('Cannot complete FileReader read twice!');
}

isDone = true;

const { error, result } = reader;

if (typeof result === 'string') {
resolve(result);
} else if (error != null) {
reject(error);
} else {
throw new Error('Unknown FileReader state');
}

reader.removeEventListener('error', complete);
reader.removeEventListener('load', complete);
};

reader.addEventListener('error', complete);
reader.addEventListener('load', complete);

reader.readAsText(blob);
});
};
} else {
getBlobText = (blob) => blob.text();
}

export { getBlobText };
26 changes: 26 additions & 0 deletions packages/common/test/lib/array/insert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { insertAtIndex } from '../../../src/lib/array/insert.ts';

describe('Array insertion', () => {
describe('at a specified index', () => {
it.each([
{ currentValues: [0, 1, 2], value: 3, insertionIndex: 3, expected: [0, 1, 2, 3] },
{ currentValues: [0, 1, 2], value: 3, insertionIndex: 1, expected: [0, 3, 1, 2] },
])(
'produces an array $expected with the inserted value $value at the specified index $insertionIndex',
({ currentValues, value, insertionIndex, expected }) => {
const result = insertAtIndex(currentValues, insertionIndex, value);

expect(result).toEqual(expected);
}
);

it("fails to insert a value at an index past the array's current length", () => {
const fn = () => {
insertAtIndex([0, 1, 2], 4, 3);
};

expect(fn).toThrow();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { UpsertableMap } from './UpsertableMap.ts';
import { UpsertableMap } from '../../../src/lib/collections/UpsertableMap.ts';

describe('UpsertableMap', () => {
class Key {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { UpsertableWeakMap } from './UpsertableWeakMap.ts';
import { UpsertableWeakMap } from '../../../src/lib/collections/UpsertableWeakMap.ts';

describe('UpsertableWeakMap', () => {
class Key {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { ScopedElementLookup } from './compatibility.ts';
import { ScopedElementLookup } from '../../../src/lib/dom/compatibility.ts';

describe('DOM compatibility library functions', () => {
describe('querying direct children of an element', () => {
Expand Down
11 changes: 11 additions & 0 deletions packages/common/test/lib/web-compat/blob.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';
import { getBlobText } from '../../../src/lib/web-compat/blob.ts';

describe('Blob compatibility', () => {
it('gets the text of a blob', async () => {
const blob = new Blob(['a']);
const text = await getBlobText(blob);

expect(text).toBe('a');
});
});
2 changes: 1 addition & 1 deletion packages/common/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts", "test/**/*.ts", "types/**/*.ts", "vite-env.d.ts"],
"include": ["src/**/*.ts", "test", "types/**/*.ts", "vite-env.d.ts"],
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true
Expand Down
13 changes: 13 additions & 0 deletions packages/common/types/Primitive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Any primitive runtime value.
*/
// prettier-ignore
export type Primitive =
// eslint-disable-next-line @typescript-eslint/sort-type-constituents
| boolean
| bigint
| number
| string
| symbol
| null
| undefined;
31 changes: 31 additions & 0 deletions packages/common/types/helpers.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
/**
* Potentially useful for simplifying some types, as exposed by TypeScript
* diagnostics and in-editor documentation.
*/
export type Identity<T> = T;

/**
* Simplifies complex types like intersections and interface extensions, as
* exposed by TypeScript diagnostics and in-editor documentation.
*
* Warnings:
*
* - Simplifications are not applied recursively
* -
*/
export type Merge<T> = Identity<{
[K in keyof T]: T[K];
}>;

/**
* May be used to simplify union types, as exposed by TypeScript diagnostics and
* in-editor documentation.
*/
export type ExpandUnion<T> = Exclude<T, never>;

/**
* Maps an object type to a shallowly-mutable type otherwise of the same shape
* and type.
*
* {@link T} should be a {@link Record}-like object. This type is **NOT
* SUITABLE** for producing mutable versions of built-in collections like
* `readonly T[]` -> `T[]` or `ReadonlyMap<T>` -> `Map<T>`.
*/
type ShallowMutable<T extends object> = {
-readonly [K in keyof T]: T[K];
};
25 changes: 25 additions & 0 deletions packages/scenario/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# @odk-web-forms/scenario (private package)

This package implements a client of [`@odk-web-forms/xforms-engine`](../xforms-engine/), and a suite of tests against that engine. These tests are either:

- Directly ported from [JavaRosa](https:/getodk/javarosa)
- Derived from those ported tests, where some environment-specific adaptation is necessary/appropriate
- Expand on JavaRosa's existing coverage using the same general testing approach

The client's name is taken from JavaRosa's [`Scenario`](https:/getodk/javarosa/blob/master/src/test/java/org/javarosa/core/test/Scenario.java). Its interface is designed to match that of JavaRosa's as well, to maximize compatibility as tests are ported updated.

## Status

Currently, very few of JavaRosa's tests have been ported. But our goal in setting up this package is to rapidly port all of the remaining tests that are appropriate for the ODK web-forms engine.

## Usage and development

As with [`@odk-web-forms/common`](../common/), this internal package is not intended to be built. Unlike that package, this one is also not intended to be used as a dependency in other packages. Usage should consist entirely of running the test suite. These tests will be run automatically in CI along with tests throughout [web-forms monorepo](../../).

To run in development, run this command at the monorepo root:

```sh
yarn workspace @odk-web-forms/scenario test
```

Individual test environments, and their corresponding watch modes, also have separate commands which can be found in [`package.json`](./package.json).
1 change: 1 addition & 0 deletions packages/scenario/do-not-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
throw new Error('Do not import from @odk-web-forms/scenario!');
Loading
Loading