@serverkgg/bridge 0.0.10 → 0.0.12
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/package.json +3 -1
- package/src/blog/articleFrontmatter.test.ts +95 -0
- package/src/blog/articleFrontmatter.ts +99 -0
- package/src/blog/compile.test.ts +148 -0
- package/src/blog/compile.ts +172 -0
- package/src/blog/compiled.ts +22 -0
- package/src/blog/index.ts +3 -0
- package/src/cli/validate.test.ts +116 -1
- package/src/cli/validate.ts +57 -4
- package/src/context/bridgeContext.ts +20 -0
- package/src/guides/compile.ts +70 -54
- package/src/manifest/guide.ts +1 -0
- package/src/manifest/manifest.test.ts +33 -0
- package/src/panel/bridgeControl.ts +1 -0
- package/src/panel/bridgeDetail.ts +1 -0
- package/src/panel/bridgePanel.ts +2 -0
- package/src/probes/index.ts +9 -0
- package/src/probes/minecraftPing.test.ts +344 -0
- package/src/probes/minecraftPing.ts +283 -0
- package/src/probes/raknetPing.test.ts +197 -0
- package/src/probes/raknetPing.ts +228 -0
- package/src/protocol/bridgeLimits.ts +8 -0
- package/src/runtime/bridgeDriver.test.ts +240 -0
- package/src/runtime/bridgeDriver.ts +137 -0
- package/src/runtime/bridgeProbe.ts +13 -1
- package/src/runtime/bridgeSetup.test.ts +387 -1
- package/src/runtime/bridgeSetup.ts +247 -7
- package/src/runtime/bridgeSetupHandlers.test.ts +303 -0
- package/src/runtime/bridgeSetupHandlers.ts +71 -0
- package/src/runtime/serve.ts +8 -41
- package/src/wire/panel.test.ts +62 -0
- package/src/wire/panel.ts +3 -0
|
@@ -9,6 +9,7 @@ import { GuideOpenTab } from "../guides";
|
|
|
9
9
|
import type { ManifestPort } from "../manifest";
|
|
10
10
|
import type { BridgeDriver, BridgePanelModule } from "../modules";
|
|
11
11
|
import {
|
|
12
|
+
BridgeControl,
|
|
12
13
|
type BridgeField,
|
|
13
14
|
BridgeFormTarget,
|
|
14
15
|
BridgeLayout,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
BRIDGE_MODULES_LIMIT,
|
|
33
34
|
BRIDGE_OPTION_VALUE_LENGTH_LIMIT,
|
|
34
35
|
BRIDGE_PANEL_TABS_LIMIT,
|
|
36
|
+
BRIDGE_PATTERN_LENGTH_LIMIT,
|
|
35
37
|
BRIDGE_PLACEHOLDER_LENGTH_LIMIT,
|
|
36
38
|
BRIDGE_ROW_ACTIONS_LIMIT,
|
|
37
39
|
BRIDGE_SECTION_ACTIONS_LIMIT,
|
|
@@ -121,6 +123,110 @@ const moduleOf = (modules: Record<string, BridgePanelModule>, id: string, where:
|
|
|
121
123
|
return module as BridgePanelModule;
|
|
122
124
|
};
|
|
123
125
|
|
|
126
|
+
const compilePattern = (pattern: string): RegExp | null => {
|
|
127
|
+
try {
|
|
128
|
+
return new RegExp(pattern);
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const OPEN_ENDED_REPEAT = /^\{\d+,\}/;
|
|
135
|
+
|
|
136
|
+
const repeatsWithoutBound = (source: string): boolean => {
|
|
137
|
+
let index = 0;
|
|
138
|
+
let inClass = false;
|
|
139
|
+
|
|
140
|
+
while (index < source.length) {
|
|
141
|
+
const char = source[index];
|
|
142
|
+
|
|
143
|
+
if (char === "\\") {
|
|
144
|
+
index += 2;
|
|
145
|
+
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (inClass) {
|
|
150
|
+
inClass = char !== "]";
|
|
151
|
+
index += 1;
|
|
152
|
+
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (char === "[") {
|
|
157
|
+
inClass = true;
|
|
158
|
+
index += 1;
|
|
159
|
+
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (char === "*" || char === "+" || (char === "{" && OPEN_ENDED_REPEAT.test(source.slice(index)))) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
index += 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return false;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const nestsQuantifiers = (pattern: string): boolean => {
|
|
174
|
+
const opened: number[] = [];
|
|
175
|
+
|
|
176
|
+
let index = 0;
|
|
177
|
+
let inClass = false;
|
|
178
|
+
|
|
179
|
+
while (index < pattern.length) {
|
|
180
|
+
const char = pattern[index];
|
|
181
|
+
|
|
182
|
+
if (char === "\\") {
|
|
183
|
+
index += 2;
|
|
184
|
+
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (inClass) {
|
|
189
|
+
inClass = char !== "]";
|
|
190
|
+
index += 1;
|
|
191
|
+
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (char === "[") {
|
|
196
|
+
inClass = true;
|
|
197
|
+
index += 1;
|
|
198
|
+
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (char === "(") {
|
|
203
|
+
opened.push(index);
|
|
204
|
+
index += 1;
|
|
205
|
+
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (char === ")") {
|
|
210
|
+
const start = opened.pop();
|
|
211
|
+
|
|
212
|
+
index += 1;
|
|
213
|
+
|
|
214
|
+
const quantified =
|
|
215
|
+
pattern[index] === "*" || pattern[index] === "+" || OPEN_ENDED_REPEAT.test(pattern.slice(index));
|
|
216
|
+
|
|
217
|
+
if (start !== undefined && quantified && repeatsWithoutBound(pattern.slice(start + 1, index - 1))) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
index += 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return false;
|
|
228
|
+
};
|
|
229
|
+
|
|
124
230
|
const validateVisibleWhen = (visibleWhen: BridgeVisibleWhen | undefined, where: string) => {
|
|
125
231
|
if (!visibleWhen) {
|
|
126
232
|
return;
|
|
@@ -149,12 +255,39 @@ const validateFields = (modules: Record<string, BridgePanelModule>, fields: Brid
|
|
|
149
255
|
validateText(field.label, "label", at);
|
|
150
256
|
validateText(field.help, "help", at);
|
|
151
257
|
validateText(field.warning, "warning", at);
|
|
258
|
+
validateText(field.patternHint, "pattern hint", at);
|
|
152
259
|
validateVisibleWhen(field.visibleWhen, at);
|
|
153
260
|
|
|
154
261
|
if (field.placeholder) {
|
|
155
262
|
lengthIn(field.placeholder, BRIDGE_PLACEHOLDER_LENGTH_LIMIT, "placeholder", at);
|
|
156
263
|
}
|
|
157
264
|
|
|
265
|
+
if (field.pattern !== undefined) {
|
|
266
|
+
if (field.control !== BridgeControl.Text && field.control !== BridgeControl.Secret) {
|
|
267
|
+
fail(`${at} declares a pattern on a "${field.control}" control — only text and secret controls match one`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
lengthIn(field.pattern, BRIDGE_PATTERN_LENGTH_LIMIT, "pattern", at);
|
|
271
|
+
|
|
272
|
+
if (!compilePattern(field.pattern)) {
|
|
273
|
+
fail(`${at} declares pattern "${field.pattern}", which is not a valid regular expression`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (nestsQuantifiers(field.pattern)) {
|
|
277
|
+
fail(
|
|
278
|
+
`${at} declares pattern "${field.pattern}", which repeats a group that already repeats — a value can make it run for minutes`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (field.patternHint && field.pattern === undefined) {
|
|
284
|
+
fail(`${at} declares a pattern hint with no pattern to explain`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (field.control === BridgeControl.Secret && field.options) {
|
|
288
|
+
fail(`${at} is a secret control with options — a secret is typed, never chosen from a list`);
|
|
289
|
+
}
|
|
290
|
+
|
|
158
291
|
if (Array.isArray(field.options)) {
|
|
159
292
|
countIn(field.options.length, BRIDGE_FIELD_OPTIONS_LIMIT, "options", at);
|
|
160
293
|
|
|
@@ -515,6 +648,10 @@ const validateSetup = (driver: BridgeDriver) => {
|
|
|
515
648
|
}
|
|
516
649
|
}
|
|
517
650
|
}
|
|
651
|
+
|
|
652
|
+
if (!setup.submit && setup.steps.some((step) => step.kind === BridgeSetupStepKind.Driver)) {
|
|
653
|
+
fail(`${where} declares a driver step but exports no submit — a driver step is answered through submit`);
|
|
654
|
+
}
|
|
518
655
|
};
|
|
519
656
|
|
|
520
657
|
const validateTab = (modules: Record<string, BridgePanelModule>, tab: BridgeTab) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BridgeProbes } from "../context";
|
|
2
|
-
import { probeA2s } from "../probes";
|
|
2
|
+
import { probeA2s, probeMinecraftPing, probeRaknetPing } from "../probes";
|
|
3
3
|
|
|
4
4
|
const PROBE_HOST = "127.0.0.1";
|
|
5
5
|
|
|
@@ -10,5 +10,17 @@ export const createBridgeProbes = (): BridgeProbes => {
|
|
|
10
10
|
async a2s(port) {
|
|
11
11
|
return await probeA2s(PROBE_HOST, port, PROBE_TIMEOUT_MS);
|
|
12
12
|
},
|
|
13
|
+
async minecraftPing(port) {
|
|
14
|
+
return await probeMinecraftPing(
|
|
15
|
+
{
|
|
16
|
+
address: PROBE_HOST,
|
|
17
|
+
port,
|
|
18
|
+
},
|
|
19
|
+
PROBE_TIMEOUT_MS,
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
async raknetPing(port) {
|
|
23
|
+
return await probeRaknetPing(PROBE_HOST, port, PROBE_TIMEOUT_MS);
|
|
24
|
+
},
|
|
13
25
|
};
|
|
14
26
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { BridgeSetupPromptKind, BridgeSetupStepState } from "../core";
|
|
3
3
|
import type { BridgeSetupRuntime } from "../protocol";
|
|
4
|
-
import { failedSetup, setupSummaryOf, unsupportedSetup } from "./bridgeSetup";
|
|
4
|
+
import { failedSetup, normalizeSetupRuntime, setupSummaryOf, unsupportedSetup } from "./bridgeSetup";
|
|
5
5
|
|
|
6
6
|
const text = (value: string) => ({
|
|
7
7
|
ar: value,
|
|
@@ -187,3 +187,389 @@ describe("a rejected submit", () => {
|
|
|
187
187
|
});
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
const declared: ReadonlySet<string> = new Set([
|
|
192
|
+
"sign-in",
|
|
193
|
+
"download",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
const normalizing = (runtime: BridgeSetupRuntime | null) => {
|
|
197
|
+
const reasons: string[] = [];
|
|
198
|
+
|
|
199
|
+
const normalized = normalizeSetupRuntime(runtime, {
|
|
200
|
+
declared,
|
|
201
|
+
warn: (_message, fields) => {
|
|
202
|
+
reasons.push(String(fields?.reason));
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
normalized,
|
|
208
|
+
reasons,
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const withSteps = (steps: BridgeSetupRuntime["steps"]): BridgeSetupRuntime => ({
|
|
213
|
+
steps,
|
|
214
|
+
prompt: null,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const link = (
|
|
218
|
+
prompt: Partial<
|
|
219
|
+
Extract<
|
|
220
|
+
BridgeSetupRuntime["prompt"],
|
|
221
|
+
{
|
|
222
|
+
kind: BridgeSetupPromptKind.Link;
|
|
223
|
+
}
|
|
224
|
+
>
|
|
225
|
+
>,
|
|
226
|
+
) => {
|
|
227
|
+
return runtimeWith({
|
|
228
|
+
kind: BridgeSetupPromptKind.Link,
|
|
229
|
+
message: text("sign in with your account"),
|
|
230
|
+
url: "https://hytale.com/device",
|
|
231
|
+
code: "ABCD-EFGH",
|
|
232
|
+
expiresAt: null,
|
|
233
|
+
actions: [],
|
|
234
|
+
...prompt,
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
describe("normalizing a reported runtime", () => {
|
|
239
|
+
test("has nothing to normalize while the driver reported no runtime", () => {
|
|
240
|
+
expect(
|
|
241
|
+
normalizeSetupRuntime(null, {
|
|
242
|
+
declared,
|
|
243
|
+
warn: () => {
|
|
244
|
+
return;
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("drops a step the driver never declared", () => {
|
|
251
|
+
const { normalized, reasons } = normalizing(
|
|
252
|
+
withSteps([
|
|
253
|
+
{
|
|
254
|
+
id: "sign-in",
|
|
255
|
+
state: BridgeSetupStepState.Active,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: "invent",
|
|
259
|
+
state: BridgeSetupStepState.Pending,
|
|
260
|
+
},
|
|
261
|
+
]),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(normalized?.steps).toEqual([
|
|
265
|
+
{
|
|
266
|
+
id: "sign-in",
|
|
267
|
+
state: BridgeSetupStepState.Active,
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
270
|
+
expect(reasons).toEqual([
|
|
271
|
+
"unknown-step",
|
|
272
|
+
]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("drops the second report of the same step", () => {
|
|
276
|
+
const { normalized, reasons } = normalizing(
|
|
277
|
+
withSteps([
|
|
278
|
+
{
|
|
279
|
+
id: "sign-in",
|
|
280
|
+
state: BridgeSetupStepState.Done,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: "sign-in",
|
|
284
|
+
state: BridgeSetupStepState.Failed,
|
|
285
|
+
},
|
|
286
|
+
]),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
expect(normalized?.steps).toEqual([
|
|
290
|
+
{
|
|
291
|
+
id: "sign-in",
|
|
292
|
+
state: BridgeSetupStepState.Done,
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
expect(reasons).toEqual([
|
|
296
|
+
"duplicate-step",
|
|
297
|
+
]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("keeps the first active step and pends the rest", () => {
|
|
301
|
+
const { normalized, reasons } = normalizing(
|
|
302
|
+
withSteps([
|
|
303
|
+
{
|
|
304
|
+
id: "sign-in",
|
|
305
|
+
state: BridgeSetupStepState.Active,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "download",
|
|
309
|
+
state: BridgeSetupStepState.Active,
|
|
310
|
+
},
|
|
311
|
+
]),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(normalized?.steps).toEqual([
|
|
315
|
+
{
|
|
316
|
+
id: "sign-in",
|
|
317
|
+
state: BridgeSetupStepState.Active,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: "download",
|
|
321
|
+
state: BridgeSetupStepState.Pending,
|
|
322
|
+
},
|
|
323
|
+
]);
|
|
324
|
+
expect(reasons).toEqual([
|
|
325
|
+
"extra-active-step",
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("clamps a percent that left the scale", () => {
|
|
330
|
+
expect(
|
|
331
|
+
normalizing(
|
|
332
|
+
runtimeWith({
|
|
333
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
334
|
+
message: text("downloading the game"),
|
|
335
|
+
percent: 140,
|
|
336
|
+
detail: null,
|
|
337
|
+
}),
|
|
338
|
+
),
|
|
339
|
+
).toEqual({
|
|
340
|
+
normalized: {
|
|
341
|
+
steps: [
|
|
342
|
+
{
|
|
343
|
+
id: "sign-in",
|
|
344
|
+
state: BridgeSetupStepState.Active,
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "download",
|
|
348
|
+
state: BridgeSetupStepState.Pending,
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
prompt: {
|
|
352
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
353
|
+
message: text("downloading the game"),
|
|
354
|
+
percent: 100,
|
|
355
|
+
detail: null,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
reasons: [
|
|
359
|
+
"percent-out-of-range",
|
|
360
|
+
],
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("drops a percent that is not a number", () => {
|
|
365
|
+
const { normalized, reasons } = normalizing(
|
|
366
|
+
runtimeWith({
|
|
367
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
368
|
+
message: text("downloading the game"),
|
|
369
|
+
percent: Number.NaN,
|
|
370
|
+
detail: null,
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(normalized?.prompt).toEqual({
|
|
375
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
376
|
+
message: text("downloading the game"),
|
|
377
|
+
percent: null,
|
|
378
|
+
detail: null,
|
|
379
|
+
});
|
|
380
|
+
expect(reasons).toEqual([
|
|
381
|
+
"unusable-percent",
|
|
382
|
+
]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("cuts a detail line to the length the platform takes", () => {
|
|
386
|
+
const { normalized, reasons } = normalizing(
|
|
387
|
+
runtimeWith({
|
|
388
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
389
|
+
message: text("downloading the game"),
|
|
390
|
+
percent: null,
|
|
391
|
+
detail: "d".repeat(300),
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
expect(normalized?.prompt).toEqual({
|
|
396
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
397
|
+
message: text("downloading the game"),
|
|
398
|
+
percent: null,
|
|
399
|
+
detail: "d".repeat(256),
|
|
400
|
+
});
|
|
401
|
+
expect(reasons).toEqual([
|
|
402
|
+
"long-detail",
|
|
403
|
+
]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("drops an empty detail line", () => {
|
|
407
|
+
const { normalized, reasons } = normalizing(
|
|
408
|
+
runtimeWith({
|
|
409
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
410
|
+
message: text("downloading the game"),
|
|
411
|
+
percent: null,
|
|
412
|
+
detail: "",
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
expect(normalized?.prompt).toMatchObject({
|
|
417
|
+
detail: null,
|
|
418
|
+
});
|
|
419
|
+
expect(reasons).toEqual([
|
|
420
|
+
"empty-detail",
|
|
421
|
+
]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("drops an action whose id is not kebab-case and the duplicate beside it", () => {
|
|
425
|
+
const { normalized, reasons } = normalizing(
|
|
426
|
+
runtimeWith({
|
|
427
|
+
kind: BridgeSetupPromptKind.Action,
|
|
428
|
+
message: text("get a new code"),
|
|
429
|
+
actions: [
|
|
430
|
+
{
|
|
431
|
+
id: "renew",
|
|
432
|
+
label: text("Get a new code"),
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: "Renew Now",
|
|
436
|
+
label: text("Get a new code"),
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: "renew",
|
|
440
|
+
label: text("Get a new code"),
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect(normalized?.prompt).toEqual({
|
|
447
|
+
kind: BridgeSetupPromptKind.Action,
|
|
448
|
+
message: text("get a new code"),
|
|
449
|
+
actions: [
|
|
450
|
+
{
|
|
451
|
+
id: "renew",
|
|
452
|
+
label: text("Get a new code"),
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
});
|
|
456
|
+
expect(reasons).toEqual([
|
|
457
|
+
"unknown-action-id",
|
|
458
|
+
"duplicate-action",
|
|
459
|
+
]);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("keeps only as many actions as the platform renders", () => {
|
|
463
|
+
const { normalized, reasons } = normalizing(
|
|
464
|
+
runtimeWith({
|
|
465
|
+
kind: BridgeSetupPromptKind.Failed,
|
|
466
|
+
message: text("the sign-in expired"),
|
|
467
|
+
actions: Array.from({
|
|
468
|
+
length: 6,
|
|
469
|
+
}).map((_entry, index) => ({
|
|
470
|
+
id: `action-${index}`,
|
|
471
|
+
label: text(`Action ${index}`),
|
|
472
|
+
})),
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(normalized?.prompt?.kind).toBe(BridgeSetupPromptKind.Failed);
|
|
477
|
+
expect(normalized?.prompt && "actions" in normalized.prompt ? normalized.prompt.actions.length : 0).toBe(4);
|
|
478
|
+
expect(reasons).toEqual([
|
|
479
|
+
"extra-actions",
|
|
480
|
+
]);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("cuts a long code and drops an empty one", () => {
|
|
484
|
+
expect(
|
|
485
|
+
normalizing(
|
|
486
|
+
link({
|
|
487
|
+
code: "c".repeat(80),
|
|
488
|
+
}),
|
|
489
|
+
).normalized?.prompt,
|
|
490
|
+
).toMatchObject({
|
|
491
|
+
code: "c".repeat(64),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(
|
|
495
|
+
normalizing(
|
|
496
|
+
link({
|
|
497
|
+
code: "",
|
|
498
|
+
}),
|
|
499
|
+
).reasons,
|
|
500
|
+
).toEqual([
|
|
501
|
+
"empty-code",
|
|
502
|
+
]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("re-emits an expiry as an iso stamp and drops one it cannot read", () => {
|
|
506
|
+
expect(
|
|
507
|
+
normalizing(
|
|
508
|
+
link({
|
|
509
|
+
expiresAt: "2026-09-04T10:00:00+03:00",
|
|
510
|
+
}),
|
|
511
|
+
).normalized?.prompt,
|
|
512
|
+
).toMatchObject({
|
|
513
|
+
expiresAt: "2026-09-04T07:00:00.000Z",
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(
|
|
517
|
+
normalizing(
|
|
518
|
+
link({
|
|
519
|
+
expiresAt: "soon",
|
|
520
|
+
}),
|
|
521
|
+
).reasons,
|
|
522
|
+
).toEqual([
|
|
523
|
+
"unusable-expiry",
|
|
524
|
+
]);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("turns a link the browser cannot open into a failed prompt keeping its actions", () => {
|
|
528
|
+
const { normalized, reasons } = normalizing(
|
|
529
|
+
link({
|
|
530
|
+
url: "javascript:alert(1)",
|
|
531
|
+
actions: [
|
|
532
|
+
{
|
|
533
|
+
id: "renew",
|
|
534
|
+
label: text("Get a new code"),
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
expect(normalized?.prompt).toEqual({
|
|
541
|
+
kind: BridgeSetupPromptKind.Failed,
|
|
542
|
+
message: {
|
|
543
|
+
ar: "اللعبة رجّعت رابط دخول ما يشتغل. جرّب مرة ثانية.",
|
|
544
|
+
en: "The game reported a broken sign-in link. Try again.",
|
|
545
|
+
},
|
|
546
|
+
actions: [
|
|
547
|
+
{
|
|
548
|
+
id: "renew",
|
|
549
|
+
label: text("Get a new code"),
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
});
|
|
553
|
+
expect(reasons).toEqual([
|
|
554
|
+
"unusable-link",
|
|
555
|
+
]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("turns a url it cannot parse into a failed prompt", () => {
|
|
559
|
+
expect(
|
|
560
|
+
normalizing(
|
|
561
|
+
link({
|
|
562
|
+
url: "not a url",
|
|
563
|
+
}),
|
|
564
|
+
).normalized?.prompt?.kind,
|
|
565
|
+
).toBe(BridgeSetupPromptKind.Failed);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("leaves a healthy runtime exactly as the driver reported it", () => {
|
|
569
|
+
const source = link({});
|
|
570
|
+
const { normalized, reasons } = normalizing(source);
|
|
571
|
+
|
|
572
|
+
expect(normalized).toEqual(source);
|
|
573
|
+
expect(reasons).toEqual([]);
|
|
574
|
+
});
|
|
575
|
+
});
|