@ankhorage/contracts 26.5.0 → 26.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/flow.d.ts +239 -0
- package/dist/flow.d.ts.map +1 -0
- package/dist/flow.js +2 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/structure/generated.d.ts +2 -2
- package/dist/structure/generated.js +2 -2
- package/dist/structure/generated.js.map +1 -1
- package/dist/website.d.ts +260 -0
- package/dist/website.d.ts.map +1 -0
- package/dist/website.js +2 -0
- package/dist/website.js.map +1 -0
- package/package.json +10 -2
- package/src/flow.test.ts +218 -0
- package/src/flow.ts +254 -0
- package/src/flowPackedConsumer.test.ts +98 -0
- package/src/index.ts +2 -0
- package/src/structure/generated.ts +2 -2
- package/src/website.test.ts +39 -0
- package/src/website.ts +287 -0
package/src/flow.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { DataSchemaSlot } from './data/schemas.js';
|
|
2
|
+
import type { DataContractValue } from './data/values.js';
|
|
3
|
+
import type { SecretRef } from './secrets.js';
|
|
4
|
+
|
|
5
|
+
export type FlowId = string;
|
|
6
|
+
export type FlowNodeId = string;
|
|
7
|
+
export type FlowPortId = string;
|
|
8
|
+
export type FlowEdgeId = string;
|
|
9
|
+
export type FlowRunId = string;
|
|
10
|
+
export type FlowStepId = string;
|
|
11
|
+
export type FlowArtifactId = string;
|
|
12
|
+
export type FlowProviderCapabilityId = string;
|
|
13
|
+
|
|
14
|
+
/** A serializable reference to a provider-owned flow capability. */
|
|
15
|
+
export interface FlowProviderCapabilityRef {
|
|
16
|
+
readonly providerId: string;
|
|
17
|
+
readonly capabilityId: FlowProviderCapabilityId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A persisted artifact reference for data too large or unsuitable for inline graph data. */
|
|
21
|
+
export interface FlowArtifactRef {
|
|
22
|
+
readonly kind: 'artifact';
|
|
23
|
+
readonly artifactId: FlowArtifactId;
|
|
24
|
+
readonly mediaType?: string;
|
|
25
|
+
readonly byteLength?: number;
|
|
26
|
+
readonly checksum?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*** A schema-agnostic inline data reference.
|
|
30
|
+
*
|
|
31
|
+
* Inline values are ordinary non-secret data. Consumers must reject a value at a credential
|
|
32
|
+
* configuration position and require a `FlowSecretRef` there; TypeScript cannot prove that
|
|
33
|
+
* invariant after JSON has crossed a trust boundary.
|
|
34
|
+
*/
|
|
35
|
+
export interface FlowInlineDataRef {
|
|
36
|
+
readonly kind: 'inline';
|
|
37
|
+
readonly value: DataContractValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A schema-agnostic data reference exchanged by node ports, runs, and traces. */
|
|
41
|
+
export type FlowDataRef = FlowArtifactRef | FlowInlineDataRef;
|
|
42
|
+
|
|
43
|
+
/*** A secret-store reference that deliberately excludes secret material.
|
|
44
|
+
*
|
|
45
|
+
* @security flow.test.ts#accepts secret references but excludes inline secret material from their contract
|
|
46
|
+
*
|
|
47
|
+
* Credential configuration must use this reference. Flow consumers must validate provider
|
|
48
|
+
* configuration at their boundary, reject inline credentials, and redact secrets from errors and
|
|
49
|
+
* traces. This portable DTO cannot determine which arbitrary provider fields are credentials, so
|
|
50
|
+
* it provides no runtime validation and makes no static guarantee for untyped JSON.
|
|
51
|
+
*/
|
|
52
|
+
export interface FlowSecretRef {
|
|
53
|
+
readonly kind: 'secret';
|
|
54
|
+
readonly ref: SecretRef;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*** JSON-safe node configuration references.
|
|
58
|
+
*
|
|
59
|
+
* A configuration value is always explicitly either non-secret data or a secret-store reference;
|
|
60
|
+
* raw JSON values are intentionally not accepted. Provider consumers own validation of the
|
|
61
|
+
* declared configuration shape and credential positions before execution.
|
|
62
|
+
*/
|
|
63
|
+
export type FlowNodeConfigValue = FlowDataRef | FlowSecretRef;
|
|
64
|
+
export type FlowNodeConfig = Readonly<Record<string, FlowNodeConfigValue>>;
|
|
65
|
+
|
|
66
|
+
/** A typed node port. Direction determines whether a node consumes or produces the port value. */
|
|
67
|
+
export interface FlowPortDescriptor {
|
|
68
|
+
readonly id: FlowPortId;
|
|
69
|
+
readonly direction: 'input' | 'output';
|
|
70
|
+
readonly label?: string;
|
|
71
|
+
readonly description?: string;
|
|
72
|
+
readonly required?: boolean;
|
|
73
|
+
readonly data?: DataSchemaSlot;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One provider-backed executable step within a portable flow graph. */
|
|
77
|
+
export interface FlowNode {
|
|
78
|
+
readonly id: FlowNodeId;
|
|
79
|
+
readonly capability: FlowProviderCapabilityRef;
|
|
80
|
+
readonly label?: string;
|
|
81
|
+
readonly config?: FlowNodeConfig;
|
|
82
|
+
readonly ports: readonly FlowPortDescriptor[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/*** A condition declared on an edge without embedding executable expressions.
|
|
86
|
+
*
|
|
87
|
+
* A `value` condition evaluates `path` as an RFC 6901 JSON Pointer against the source port's
|
|
88
|
+
* JSON value; the empty string denotes the complete value. Missing-pointer evaluation does not
|
|
89
|
+
* match. `equals` uses JSON structural equality: values have the same type and recursive array
|
|
90
|
+
* order and object member/value pairs, independent of object member order. Engines must not use
|
|
91
|
+
* coercion, locale comparison, or executable expressions.
|
|
92
|
+
*/
|
|
93
|
+
export type FlowEdgeCondition =
|
|
94
|
+
| { readonly kind: 'always' }
|
|
95
|
+
| { readonly kind: 'outcome'; readonly outcome: 'failure' | 'success' }
|
|
96
|
+
| { readonly kind: 'value'; readonly path: string; readonly equals: DataContractValue };
|
|
97
|
+
|
|
98
|
+
/** A directed connection from one output port to one input port. */
|
|
99
|
+
export interface FlowEdge {
|
|
100
|
+
readonly id: FlowEdgeId;
|
|
101
|
+
readonly source: { readonly nodeId: FlowNodeId; readonly portId: FlowPortId };
|
|
102
|
+
readonly target: { readonly nodeId: FlowNodeId; readonly portId: FlowPortId };
|
|
103
|
+
readonly condition?: FlowEdgeCondition;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/*** A portable trigger declaration; runtime scheduling and delivery remain provider-owned.
|
|
107
|
+
*
|
|
108
|
+
* A schedule uses a five-field POSIX cron expression: minute, hour, day of month, month, and day
|
|
109
|
+
* of week. Seconds, years, macros, and implementation-specific extensions are invalid. `timezone`
|
|
110
|
+
* is an IANA time-zone name and defaults to `Etc/UTC`. Match it against local wall-clock time; a
|
|
111
|
+
* spring-forward nonexistent local time is skipped and each occurrence of a repeated local time
|
|
112
|
+
* during a fall-back transition fires separately. When both day-of-month and day-of-week are
|
|
113
|
+
* restricted, either match is sufficient, following POSIX cron semantics.
|
|
114
|
+
*/
|
|
115
|
+
export type FlowTrigger =
|
|
116
|
+
| { readonly id: string; readonly kind: 'manual' }
|
|
117
|
+
| {
|
|
118
|
+
readonly id: string;
|
|
119
|
+
readonly kind: 'schedule';
|
|
120
|
+
readonly cron: string;
|
|
121
|
+
readonly timezone?: string;
|
|
122
|
+
}
|
|
123
|
+
| {
|
|
124
|
+
readonly id: string;
|
|
125
|
+
readonly kind: 'event';
|
|
126
|
+
readonly capability: FlowProviderCapabilityRef;
|
|
127
|
+
readonly event: string;
|
|
128
|
+
}
|
|
129
|
+
| { readonly id: string; readonly kind: 'webhook'; readonly path: string };
|
|
130
|
+
|
|
131
|
+
/** An immutable revision of a portable workflow graph. */
|
|
132
|
+
export interface FlowDefinition {
|
|
133
|
+
readonly id: FlowId;
|
|
134
|
+
readonly version: number;
|
|
135
|
+
readonly title: string;
|
|
136
|
+
readonly description?: string;
|
|
137
|
+
readonly nodes: readonly FlowNode[];
|
|
138
|
+
readonly edges: readonly FlowEdge[];
|
|
139
|
+
readonly triggers: readonly FlowTrigger[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** An immutable workflow revision reference. */
|
|
143
|
+
export interface FlowDefinitionRef {
|
|
144
|
+
readonly id: FlowId;
|
|
145
|
+
readonly version: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type FlowExecutionStatus =
|
|
149
|
+
| 'cancelled'
|
|
150
|
+
| 'failed'
|
|
151
|
+
| 'pending'
|
|
152
|
+
| 'running'
|
|
153
|
+
| 'succeeded'
|
|
154
|
+
| 'timed_out'
|
|
155
|
+
| 'waiting_for_approval';
|
|
156
|
+
|
|
157
|
+
/** A portable error observation that never carries a host Error object. */
|
|
158
|
+
export interface FlowError {
|
|
159
|
+
readonly code: string;
|
|
160
|
+
readonly message: string;
|
|
161
|
+
readonly retryable: boolean;
|
|
162
|
+
readonly details?: FlowDataRef;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A redaction-safe trace observation emitted by a run or step. */
|
|
166
|
+
export interface FlowTraceEvent {
|
|
167
|
+
readonly timestamp: string;
|
|
168
|
+
readonly level: 'debug' | 'error' | 'info' | 'warn';
|
|
169
|
+
readonly message: string;
|
|
170
|
+
readonly attributes?: Readonly<Record<string, DataContractValue>>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/*** A durable snapshot of one execution attempt for a graph node.
|
|
174
|
+
*
|
|
175
|
+
* `attempt` is a one-based, monotonically increasing number for this run and node. A retry has a
|
|
176
|
+
* new step snapshot and attempt number; consumers must not overwrite a completed attempt.
|
|
177
|
+
*/
|
|
178
|
+
export interface FlowStepSnapshot {
|
|
179
|
+
readonly id: FlowStepId;
|
|
180
|
+
readonly nodeId: FlowNodeId;
|
|
181
|
+
readonly status: FlowExecutionStatus;
|
|
182
|
+
readonly attempt: number;
|
|
183
|
+
readonly startedAt?: string;
|
|
184
|
+
readonly completedAt?: string;
|
|
185
|
+
readonly input?: Readonly<Record<FlowPortId, FlowDataRef>>;
|
|
186
|
+
readonly output?: Readonly<Record<FlowPortId, FlowDataRef>>;
|
|
187
|
+
readonly error?: FlowError;
|
|
188
|
+
readonly trace?: readonly FlowTraceEvent[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** A stable reference to one immutable step attempt in a run. */
|
|
192
|
+
export interface FlowStepRef {
|
|
193
|
+
readonly runId: FlowRunId;
|
|
194
|
+
readonly id: FlowStepId;
|
|
195
|
+
readonly nodeId: FlowNodeId;
|
|
196
|
+
readonly attempt: number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A reference to the triggering event or request that started a run. */
|
|
200
|
+
export interface FlowRunTrigger {
|
|
201
|
+
readonly triggerId: string;
|
|
202
|
+
readonly kind: FlowTrigger['kind'];
|
|
203
|
+
readonly data?: FlowDataRef;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/*** A durable snapshot of an execution against one immutable flow revision.
|
|
207
|
+
*
|
|
208
|
+
* A run begins `pending`, may move to `running` or `cancelled`, and is terminal once
|
|
209
|
+
* `cancelled`, `failed`, `succeeded`, or `timed_out`. `waiting_for_approval` pauses an otherwise
|
|
210
|
+
* running run; resolving its pending approval resumes it or ends it as cancelled or failed.
|
|
211
|
+
* Consumers must validate these transitions and must retain the referenced definition revision.
|
|
212
|
+
*/
|
|
213
|
+
export interface FlowRunSnapshot {
|
|
214
|
+
readonly id: FlowRunId;
|
|
215
|
+
readonly flow: FlowDefinitionRef;
|
|
216
|
+
readonly status: FlowExecutionStatus;
|
|
217
|
+
readonly trigger: FlowRunTrigger;
|
|
218
|
+
readonly createdAt: string;
|
|
219
|
+
readonly startedAt?: string;
|
|
220
|
+
readonly completedAt?: string;
|
|
221
|
+
readonly input?: Readonly<Record<FlowPortId, FlowDataRef>>;
|
|
222
|
+
readonly output?: Readonly<Record<FlowPortId, FlowDataRef>>;
|
|
223
|
+
readonly steps: readonly FlowStepSnapshot[];
|
|
224
|
+
readonly error?: FlowError;
|
|
225
|
+
readonly trace?: readonly FlowTraceEvent[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** A stable reference to a run against one immutable workflow revision. */
|
|
229
|
+
export interface FlowRunRef {
|
|
230
|
+
readonly id: FlowRunId;
|
|
231
|
+
readonly flow: FlowDefinitionRef;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type FlowApprovalStatus = 'approved' | 'cancelled' | 'expired' | 'pending' | 'rejected';
|
|
235
|
+
|
|
236
|
+
/*** A serializable human-approval request associated with one durable run.
|
|
237
|
+
*
|
|
238
|
+
* An approval starts `pending` and transitions exactly once to `approved`, `rejected`, `expired`,
|
|
239
|
+
* or `cancelled`; those states are terminal. A step approval identifies its exact attempt so a
|
|
240
|
+
* later retry cannot resolve an earlier request. Consumers validate timestamps and transitions.
|
|
241
|
+
*/
|
|
242
|
+
export interface FlowApprovalRequest {
|
|
243
|
+
readonly id: string;
|
|
244
|
+
readonly runId: FlowRunId;
|
|
245
|
+
readonly step?: FlowStepRef;
|
|
246
|
+
readonly status: FlowApprovalStatus;
|
|
247
|
+
readonly requestedAt: string;
|
|
248
|
+
readonly expiresAt?: string;
|
|
249
|
+
readonly resolvedAt?: string;
|
|
250
|
+
readonly requestedBy?: string;
|
|
251
|
+
readonly resolvedBy?: string;
|
|
252
|
+
readonly message?: string;
|
|
253
|
+
readonly context?: FlowDataRef;
|
|
254
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'bun:test';
|
|
6
|
+
|
|
7
|
+
const packageRoot = process.cwd();
|
|
8
|
+
const temporaryCacheDirectory = join(tmpdir(), 'contracts-flow-consumer-cache');
|
|
9
|
+
|
|
10
|
+
const runCommand = (command: string[], cwd: string): string => {
|
|
11
|
+
const result = Bun.spawnSync(command, {
|
|
12
|
+
cwd,
|
|
13
|
+
env: {
|
|
14
|
+
...process.env,
|
|
15
|
+
BUN_INSTALL_CACHE_DIR: temporaryCacheDirectory,
|
|
16
|
+
npm_config_cache: temporaryCacheDirectory,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const output = `${result.stdout.toString()}${result.stderr.toString()}`;
|
|
20
|
+
|
|
21
|
+
expect(result.exitCode, output).toBe(0);
|
|
22
|
+
return output;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const writeConsumerFiles = (consumerDirectory: string): void => {
|
|
26
|
+
writeFileSync(
|
|
27
|
+
join(consumerDirectory, 'consumer.mjs'),
|
|
28
|
+
[
|
|
29
|
+
"import {} from '@ankhorage/contracts/flow';",
|
|
30
|
+
"console.log('published flow contract is available');",
|
|
31
|
+
].join('\n'),
|
|
32
|
+
);
|
|
33
|
+
writeFileSync(
|
|
34
|
+
join(consumerDirectory, 'consumer.ts'),
|
|
35
|
+
[
|
|
36
|
+
"import type { FlowDefinition } from '@ankhorage/contracts/flow';",
|
|
37
|
+
"const flow: FlowDefinition = { id: 'consumer-flow', version: 1, title: 'Consumer flow', nodes: [], edges: [], triggers: [{ id: 'manual', kind: 'manual' }] };",
|
|
38
|
+
'void flow;',
|
|
39
|
+
].join('\n'),
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const packAndInstall = (consumerDirectory: string, packedDirectory: string): void => {
|
|
44
|
+
runCommand(['bun', 'run', 'build'], packageRoot);
|
|
45
|
+
runCommand(
|
|
46
|
+
['npm', 'pack', '--ignore-scripts', '--pack-destination', packedDirectory],
|
|
47
|
+
packageRoot,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const archiveName = readdirSync(packedDirectory).find((entry) => entry.endsWith('.tgz'));
|
|
51
|
+
expect(archiveName).toBeDefined();
|
|
52
|
+
runCommand(
|
|
53
|
+
['bun', 'add', '--ignore-scripts', join(packedDirectory, archiveName ?? 'missing.tgz')],
|
|
54
|
+
consumerDirectory,
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const verifyConsumer = (consumerDirectory: string): void => {
|
|
59
|
+
runCommand(['node', 'consumer.mjs'], consumerDirectory);
|
|
60
|
+
runCommand(['bun', 'consumer.mjs'], consumerDirectory);
|
|
61
|
+
runCommand(
|
|
62
|
+
[
|
|
63
|
+
join(packageRoot, 'node_modules/.bin/tsc'),
|
|
64
|
+
'--noEmit',
|
|
65
|
+
'--module',
|
|
66
|
+
'NodeNext',
|
|
67
|
+
'--moduleResolution',
|
|
68
|
+
'NodeNext',
|
|
69
|
+
'--target',
|
|
70
|
+
'ES2022',
|
|
71
|
+
'--skipLibCheck',
|
|
72
|
+
'consumer.ts',
|
|
73
|
+
],
|
|
74
|
+
consumerDirectory,
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
describe('packed Flow consumer', () => {
|
|
79
|
+
it('supports native Node ESM, Bun, and public declarations', () => {
|
|
80
|
+
const temporaryDirectory = mkdtempSync(join(tmpdir(), 'contracts-flow-consumer-'));
|
|
81
|
+
const consumerDirectory = join(temporaryDirectory, 'package');
|
|
82
|
+
const packedDirectory = join(temporaryDirectory, 'packed');
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
mkdirSync(consumerDirectory);
|
|
86
|
+
mkdirSync(packedDirectory);
|
|
87
|
+
writeFileSync(
|
|
88
|
+
join(consumerDirectory, 'package.json'),
|
|
89
|
+
JSON.stringify({ name: 'contracts-flow-consumer', private: true, type: 'module' }),
|
|
90
|
+
);
|
|
91
|
+
writeConsumerFiles(consumerDirectory);
|
|
92
|
+
packAndInstall(consumerDirectory, packedDirectory);
|
|
93
|
+
verifyConsumer(consumerDirectory);
|
|
94
|
+
} finally {
|
|
95
|
+
rmSync(temporaryDirectory, { force: true, recursive: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './db.js';
|
|
|
9
9
|
export * from './deploy.js';
|
|
10
10
|
export * from './deployProvider.js';
|
|
11
11
|
export * from './environments.js';
|
|
12
|
+
export * from './flow.js';
|
|
12
13
|
export * from './infra.js';
|
|
13
14
|
export * from './media.js';
|
|
14
15
|
export * from './navigator.js';
|
|
@@ -24,3 +25,4 @@ export * from './structure/index.js';
|
|
|
24
25
|
export * from './theme.js';
|
|
25
26
|
export * from './types.js';
|
|
26
27
|
export * from './ui.js';
|
|
28
|
+
export * from './website.js';
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import type { StructureDescriptorDocument } from '@ankhorage/contracts/structure';
|
|
3
3
|
|
|
4
4
|
export const STRUCTURE_DESCRIPTOR_COMPILER_VERSION = '6.0.3';
|
|
5
|
-
export const STRUCTURE_DESCRIPTOR_FINGERPRINT = 'sha256:
|
|
5
|
+
export const STRUCTURE_DESCRIPTOR_FINGERPRINT = 'sha256:67f55755ba8bc7a6ba9239317adbad41d62c006976654188a9f817e3c807509a';
|
|
6
6
|
|
|
7
7
|
export const STRUCTURE_DESCRIPTOR = {
|
|
8
8
|
"protocolVersion": 1,
|
|
9
9
|
"packageName": "@ankhorage/contracts",
|
|
10
|
-
"packageVersion": "26.
|
|
10
|
+
"packageVersion": "26.7.0",
|
|
11
11
|
"roots": {
|
|
12
12
|
"auth-flow": "AuthFlowConfig",
|
|
13
13
|
"auth-profile": "AuthProfileSpec",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import phpObservation from '../test/website/fixtures/php-observation.json' with { type: 'json' };
|
|
4
|
+
import phpRecognition from '../test/website/fixtures/php-recognition.json' with { type: 'json' };
|
|
5
|
+
import typescriptObservation from '../test/website/fixtures/typescript-observation.json' with { type: 'json' };
|
|
6
|
+
import typescriptRecognition from '../test/website/fixtures/typescript-recognition.json' with { type: 'json' };
|
|
7
|
+
import type { WebsiteObservationDocument, WebsiteRecognitionDocument } from './website.js';
|
|
8
|
+
|
|
9
|
+
const websiteObservationFixture = typescriptObservation as unknown as WebsiteObservationDocument;
|
|
10
|
+
const websiteRecognitionFixture = typescriptRecognition as unknown as WebsiteRecognitionDocument;
|
|
11
|
+
|
|
12
|
+
it('keeps TypeScript and PHP website observation fixtures identical and serializable', () => {
|
|
13
|
+
expect(typescriptObservation).toEqual(phpObservation);
|
|
14
|
+
expect(typescriptObservation.pages).toHaveLength(2);
|
|
15
|
+
expect(typescriptObservation.pages[0]?.viewports).toHaveLength(3);
|
|
16
|
+
expect(JSON.parse(JSON.stringify(websiteObservationFixture))).toEqual(websiteObservationFixture);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('keeps TypeScript and PHP website recognition fixtures identical and serializable', () => {
|
|
20
|
+
expect(typescriptRecognition).toEqual(phpRecognition);
|
|
21
|
+
expect(typescriptRecognition.pages[0]?.root.children).toHaveLength(4);
|
|
22
|
+
expect(JSON.parse(JSON.stringify(websiteRecognitionFixture))).toEqual(websiteRecognitionFixture);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('retains the visible unresolved node and the styled inline text run', () => {
|
|
26
|
+
expect(
|
|
27
|
+
websiteObservationFixture.pages[0]?.nodes.find((node) => node.sourceRef === 'home:hero:span'),
|
|
28
|
+
).toMatchObject({ textRuns: [{ text: 'red styled text' }] });
|
|
29
|
+
expect(
|
|
30
|
+
websiteRecognitionFixture.pages[0]?.nodes.find((node) => node.status === 'unresolved'),
|
|
31
|
+
).toMatchObject({ sourceRefs: ['home:visible-unresolved'] });
|
|
32
|
+
expect(websiteRecognitionFixture.pages[0]?.root.children?.at(-1)).toMatchObject({
|
|
33
|
+
id: 'home:visible-unresolved',
|
|
34
|
+
});
|
|
35
|
+
expect(websiteRecognitionFixture.pages[0]?.root.children?.[0]?.children?.[1]).toMatchObject({
|
|
36
|
+
id: 'home:rich-text',
|
|
37
|
+
type: 'RichText',
|
|
38
|
+
});
|
|
39
|
+
});
|