@stardeck-customer-apps/testing 0.4.0 → 0.5.1
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/SKILL.md +33 -1
- package/dist/index.d.mts +107 -1
- package/dist/index.d.ts +107 -1
- package/dist/index.js +238 -4
- package/dist/index.mjs +238 -4
- package/dist/next/headers-shim.js +7 -0
- package/dist/next/headers-shim.mjs +7 -0
- package/dist/setup.js +188 -4
- package/dist/setup.mjs +188 -4
- package/package.json +6 -1
package/SKILL.md
CHANGED
|
@@ -53,7 +53,7 @@ beforeEach(() => app.reset());
|
|
|
53
53
|
afterAll(() => app.close());
|
|
54
54
|
|
|
55
55
|
describeWorkflow("checkout", () => {
|
|
56
|
-
it("
|
|
56
|
+
it("someone buys a widget and gets a confirmation email", async () => {
|
|
57
57
|
const user = app.asUser({ email: "buyer@example.com" });
|
|
58
58
|
|
|
59
59
|
const res = await callRoute(checkout, { body: { items: [{ sku: "Widget", qty: 2 }] } });
|
|
@@ -101,6 +101,15 @@ expect(app.storage.latest()?.filename).toBe("note.txt");
|
|
|
101
101
|
const integrations = createIntegrationsClient();
|
|
102
102
|
await integrations.line.push("U123", { type: "text", text: "Your order shipped" });
|
|
103
103
|
expect(app.messages.channel("line").to("U123")[0].body.text).toMatch(/shipped/i);
|
|
104
|
+
|
|
105
|
+
// Receipt print + display
|
|
106
|
+
import { createEdgeClient } from "@stardeck-customer-apps/edge-sdk/server";
|
|
107
|
+
|
|
108
|
+
const edge = createEdgeClient();
|
|
109
|
+
await edge.print({ receipt: { total: 42 }, alias: "station-1", openDrawer: true });
|
|
110
|
+
expect(app.edge.latestPrint()?.receipt.total).toBe(42);
|
|
111
|
+
await edge.showDisplay({ alias: "customer-screen", url: "https://app.example.com/kiosk" });
|
|
112
|
+
expect(app.edge.latestDisplay()?.action).toBe("show");
|
|
104
113
|
```
|
|
105
114
|
|
|
106
115
|
## API
|
|
@@ -131,6 +140,9 @@ expect(app.messages.channel("line").to("U123")[0].body.text).toMatch(/shipped/i)
|
|
|
131
140
|
- `app.storage` — uploads through storage-sdk: `.uploads`, `.latest()`, `.count`, `.clear()`.
|
|
132
141
|
- `app.messages` — outbound Slack/LINE/Facebook sends:
|
|
133
142
|
`.all()`, `.latest()`, `.to(recipient)`, `.channel("line")`, `.count`, `.clear()`.
|
|
143
|
+
- `app.edge` — edge print/display/bindings from edge-sdk: `.prints`, `.displays`,
|
|
144
|
+
`.testPrints`, `.latestPrint()`, `.latestDisplay()`, `.bindings`,
|
|
145
|
+
`.seedDevices()`, `.seedPeripherals()`, `.seedBindings()`, `.count`, `.clear()`.
|
|
134
146
|
- `app.query(sql, params?)` / `app.db` — direct database access for
|
|
135
147
|
assertions and seeding.
|
|
136
148
|
- `callRoute(handler, opts)` — invoke an App Router route handler with a real
|
|
@@ -141,6 +153,23 @@ expect(app.messages.channel("line").to("U123")[0].body.text).toMatch(/shipped/i)
|
|
|
141
153
|
("checkout", "booking", "inventory"). The platform reports these per
|
|
142
154
|
deployment — every critical workflow should have one.
|
|
143
155
|
|
|
156
|
+
### Naming
|
|
157
|
+
|
|
158
|
+
The app owner reads these names in the dashboard, under the workflow, without
|
|
159
|
+
ever opening the code. Name the workflow after the business capability, and name
|
|
160
|
+
each test after the thing a person does, in a full sentence:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
describeWorkflow("checkout", () => {
|
|
164
|
+
it("someone adds an item to their cart", ...);
|
|
165
|
+
it("someone pays for their cart with a credit card", ...);
|
|
166
|
+
it("someone whose card is declined keeps their cart", ...);
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Not `it("POST /api/checkout returns 200")` or `it("decrements stock")` — those
|
|
171
|
+
describe the code, and the owner can't tell from them what is or isn't covered.
|
|
172
|
+
|
|
144
173
|
## What works out of the box
|
|
145
174
|
|
|
146
175
|
- `DataStoreClient` (query/insert/update/delete/schema ops) — served by the
|
|
@@ -161,6 +190,9 @@ expect(app.messages.channel("line").to("U123")[0].body.text).toMatch(/shipped/i)
|
|
|
161
190
|
in `app.storage` against the simulated `STORAGE_URL` host.
|
|
162
191
|
- `client.slack` / `client.line` / `client.facebook` send endpoints — captured
|
|
163
192
|
in `app.messages`.
|
|
193
|
+
- `createEdgeClient()` (print, pair/unpair, list devices/peripherals, show/clear
|
|
194
|
+
display, test print) — captured in `app.edge`; seed devices/peripherals for
|
|
195
|
+
pairing pickers via `app.edge.seedDevices()` / `seedPeripherals()`.
|
|
164
196
|
- `next/headers` (`headers()`/`cookies()`) inside handlers under `callRoute`.
|
|
165
197
|
|
|
166
198
|
## Rules
|
package/dist/index.d.mts
CHANGED
|
@@ -184,6 +184,106 @@ interface TestMessages {
|
|
|
184
184
|
clear(): void;
|
|
185
185
|
get count(): number;
|
|
186
186
|
}
|
|
187
|
+
interface ReceiptPayload {
|
|
188
|
+
header?: {
|
|
189
|
+
lines: string[];
|
|
190
|
+
};
|
|
191
|
+
items?: Array<{
|
|
192
|
+
name: string;
|
|
193
|
+
quantity: number;
|
|
194
|
+
unitPrice: number;
|
|
195
|
+
total: number;
|
|
196
|
+
note?: string;
|
|
197
|
+
}>;
|
|
198
|
+
subtotal?: number;
|
|
199
|
+
tax?: number;
|
|
200
|
+
discount?: number;
|
|
201
|
+
total?: number;
|
|
202
|
+
payments?: Array<{
|
|
203
|
+
method: string;
|
|
204
|
+
amount: number;
|
|
205
|
+
}>;
|
|
206
|
+
footer?: {
|
|
207
|
+
lines: string[];
|
|
208
|
+
};
|
|
209
|
+
barcode?: {
|
|
210
|
+
type: "qr" | "code128";
|
|
211
|
+
data: string;
|
|
212
|
+
};
|
|
213
|
+
rawEscPos?: string;
|
|
214
|
+
}
|
|
215
|
+
interface DeviceInfo {
|
|
216
|
+
id: string;
|
|
217
|
+
displayName: string;
|
|
218
|
+
status: "pairing_pending" | "online" | "offline" | "decommissioned";
|
|
219
|
+
isDefault: boolean;
|
|
220
|
+
lastHeartbeatAt: string | null;
|
|
221
|
+
}
|
|
222
|
+
interface PeripheralInfo {
|
|
223
|
+
id: string;
|
|
224
|
+
displayName: string | null;
|
|
225
|
+
driver: string | null;
|
|
226
|
+
transport: "usb_lp" | "serial" | "tcp" | "usb_raw" | "display";
|
|
227
|
+
connected: boolean;
|
|
228
|
+
lastSeenAt: string | null;
|
|
229
|
+
device: {
|
|
230
|
+
id: string;
|
|
231
|
+
displayName: string;
|
|
232
|
+
status: DeviceInfo["status"];
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
type BindingState = "ok" | "peripheral_missing" | "grant_revoked";
|
|
236
|
+
interface BindingInfo {
|
|
237
|
+
alias: string;
|
|
238
|
+
state: BindingState;
|
|
239
|
+
peripheral: {
|
|
240
|
+
id: string;
|
|
241
|
+
displayName: string | null;
|
|
242
|
+
driver: string | null;
|
|
243
|
+
connected: boolean;
|
|
244
|
+
} | null;
|
|
245
|
+
device: {
|
|
246
|
+
id: string;
|
|
247
|
+
displayName: string;
|
|
248
|
+
status: DeviceInfo["status"];
|
|
249
|
+
} | null;
|
|
250
|
+
updatedAt: string;
|
|
251
|
+
}
|
|
252
|
+
interface CapturedPrint {
|
|
253
|
+
jobId: string;
|
|
254
|
+
deploymentId: string;
|
|
255
|
+
alias?: string;
|
|
256
|
+
deviceId?: string;
|
|
257
|
+
peripheralId?: string;
|
|
258
|
+
receipt: ReceiptPayload;
|
|
259
|
+
openDrawer: boolean;
|
|
260
|
+
logo?: boolean;
|
|
261
|
+
copies: number;
|
|
262
|
+
}
|
|
263
|
+
interface CapturedDisplay {
|
|
264
|
+
action: "show" | "clear";
|
|
265
|
+
alias?: string;
|
|
266
|
+
peripheralId?: string;
|
|
267
|
+
url?: string;
|
|
268
|
+
}
|
|
269
|
+
interface CapturedTestPrint {
|
|
270
|
+
alias?: string;
|
|
271
|
+
deviceId?: string;
|
|
272
|
+
peripheralId?: string;
|
|
273
|
+
}
|
|
274
|
+
interface TestEdge {
|
|
275
|
+
get prints(): CapturedPrint[];
|
|
276
|
+
get displays(): CapturedDisplay[];
|
|
277
|
+
get testPrints(): CapturedTestPrint[];
|
|
278
|
+
latestPrint(): CapturedPrint | undefined;
|
|
279
|
+
latestDisplay(): CapturedDisplay | undefined;
|
|
280
|
+
get bindings(): BindingInfo[];
|
|
281
|
+
seedDevices(devices: DeviceInfo[]): void;
|
|
282
|
+
seedPeripherals(peripherals: PeripheralInfo[]): void;
|
|
283
|
+
seedBindings(bindings: BindingInfo[]): void;
|
|
284
|
+
clear(): void;
|
|
285
|
+
get count(): number;
|
|
286
|
+
}
|
|
187
287
|
interface TestAppOptions {
|
|
188
288
|
/**
|
|
189
289
|
* Path to the schema.sql snapshot generated by
|
|
@@ -219,6 +319,8 @@ interface TestApp {
|
|
|
219
319
|
storage: TestStorage;
|
|
220
320
|
/** Captured outbound messages from integrations-sdk messaging channels. */
|
|
221
321
|
messages: TestMessages;
|
|
322
|
+
/** Captured edge print/display ops and peripheral bindings from edge-sdk. */
|
|
323
|
+
edge: TestEdge;
|
|
222
324
|
/** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
|
|
223
325
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
224
326
|
/**
|
|
@@ -251,6 +353,10 @@ declare function createTestApp(options?: TestAppOptions): Promise<TestApp>;
|
|
|
251
353
|
* "booking", "inventory"). The platform parses the `workflow:` prefix out of
|
|
252
354
|
* test reporter output to show per-workflow pass/fail on deployments — use
|
|
253
355
|
* one block per workflow the app's owner cares about.
|
|
356
|
+
*
|
|
357
|
+
* The dashboard lists the workflow's test names beneath it, so title each test
|
|
358
|
+
* as a sentence about what a person does ("someone pays with a credit card"),
|
|
359
|
+
* not about the code under it.
|
|
254
360
|
*/
|
|
255
361
|
declare function describeWorkflow(name: string, fn: () => void): void;
|
|
256
362
|
declare const WORKFLOW_NAME_PREFIX = "workflow:";
|
|
@@ -273,4 +379,4 @@ declare const TEST_ENV_DEFAULTS: {
|
|
|
273
379
|
/** Default location of the DDL snapshot written by `generate-types`. */
|
|
274
380
|
declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
275
381
|
|
|
276
|
-
export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedUpload, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
|
382
|
+
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
package/dist/index.d.ts
CHANGED
|
@@ -184,6 +184,106 @@ interface TestMessages {
|
|
|
184
184
|
clear(): void;
|
|
185
185
|
get count(): number;
|
|
186
186
|
}
|
|
187
|
+
interface ReceiptPayload {
|
|
188
|
+
header?: {
|
|
189
|
+
lines: string[];
|
|
190
|
+
};
|
|
191
|
+
items?: Array<{
|
|
192
|
+
name: string;
|
|
193
|
+
quantity: number;
|
|
194
|
+
unitPrice: number;
|
|
195
|
+
total: number;
|
|
196
|
+
note?: string;
|
|
197
|
+
}>;
|
|
198
|
+
subtotal?: number;
|
|
199
|
+
tax?: number;
|
|
200
|
+
discount?: number;
|
|
201
|
+
total?: number;
|
|
202
|
+
payments?: Array<{
|
|
203
|
+
method: string;
|
|
204
|
+
amount: number;
|
|
205
|
+
}>;
|
|
206
|
+
footer?: {
|
|
207
|
+
lines: string[];
|
|
208
|
+
};
|
|
209
|
+
barcode?: {
|
|
210
|
+
type: "qr" | "code128";
|
|
211
|
+
data: string;
|
|
212
|
+
};
|
|
213
|
+
rawEscPos?: string;
|
|
214
|
+
}
|
|
215
|
+
interface DeviceInfo {
|
|
216
|
+
id: string;
|
|
217
|
+
displayName: string;
|
|
218
|
+
status: "pairing_pending" | "online" | "offline" | "decommissioned";
|
|
219
|
+
isDefault: boolean;
|
|
220
|
+
lastHeartbeatAt: string | null;
|
|
221
|
+
}
|
|
222
|
+
interface PeripheralInfo {
|
|
223
|
+
id: string;
|
|
224
|
+
displayName: string | null;
|
|
225
|
+
driver: string | null;
|
|
226
|
+
transport: "usb_lp" | "serial" | "tcp" | "usb_raw" | "display";
|
|
227
|
+
connected: boolean;
|
|
228
|
+
lastSeenAt: string | null;
|
|
229
|
+
device: {
|
|
230
|
+
id: string;
|
|
231
|
+
displayName: string;
|
|
232
|
+
status: DeviceInfo["status"];
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
type BindingState = "ok" | "peripheral_missing" | "grant_revoked";
|
|
236
|
+
interface BindingInfo {
|
|
237
|
+
alias: string;
|
|
238
|
+
state: BindingState;
|
|
239
|
+
peripheral: {
|
|
240
|
+
id: string;
|
|
241
|
+
displayName: string | null;
|
|
242
|
+
driver: string | null;
|
|
243
|
+
connected: boolean;
|
|
244
|
+
} | null;
|
|
245
|
+
device: {
|
|
246
|
+
id: string;
|
|
247
|
+
displayName: string;
|
|
248
|
+
status: DeviceInfo["status"];
|
|
249
|
+
} | null;
|
|
250
|
+
updatedAt: string;
|
|
251
|
+
}
|
|
252
|
+
interface CapturedPrint {
|
|
253
|
+
jobId: string;
|
|
254
|
+
deploymentId: string;
|
|
255
|
+
alias?: string;
|
|
256
|
+
deviceId?: string;
|
|
257
|
+
peripheralId?: string;
|
|
258
|
+
receipt: ReceiptPayload;
|
|
259
|
+
openDrawer: boolean;
|
|
260
|
+
logo?: boolean;
|
|
261
|
+
copies: number;
|
|
262
|
+
}
|
|
263
|
+
interface CapturedDisplay {
|
|
264
|
+
action: "show" | "clear";
|
|
265
|
+
alias?: string;
|
|
266
|
+
peripheralId?: string;
|
|
267
|
+
url?: string;
|
|
268
|
+
}
|
|
269
|
+
interface CapturedTestPrint {
|
|
270
|
+
alias?: string;
|
|
271
|
+
deviceId?: string;
|
|
272
|
+
peripheralId?: string;
|
|
273
|
+
}
|
|
274
|
+
interface TestEdge {
|
|
275
|
+
get prints(): CapturedPrint[];
|
|
276
|
+
get displays(): CapturedDisplay[];
|
|
277
|
+
get testPrints(): CapturedTestPrint[];
|
|
278
|
+
latestPrint(): CapturedPrint | undefined;
|
|
279
|
+
latestDisplay(): CapturedDisplay | undefined;
|
|
280
|
+
get bindings(): BindingInfo[];
|
|
281
|
+
seedDevices(devices: DeviceInfo[]): void;
|
|
282
|
+
seedPeripherals(peripherals: PeripheralInfo[]): void;
|
|
283
|
+
seedBindings(bindings: BindingInfo[]): void;
|
|
284
|
+
clear(): void;
|
|
285
|
+
get count(): number;
|
|
286
|
+
}
|
|
187
287
|
interface TestAppOptions {
|
|
188
288
|
/**
|
|
189
289
|
* Path to the schema.sql snapshot generated by
|
|
@@ -219,6 +319,8 @@ interface TestApp {
|
|
|
219
319
|
storage: TestStorage;
|
|
220
320
|
/** Captured outbound messages from integrations-sdk messaging channels. */
|
|
221
321
|
messages: TestMessages;
|
|
322
|
+
/** Captured edge print/display ops and peripheral bindings from edge-sdk. */
|
|
323
|
+
edge: TestEdge;
|
|
222
324
|
/** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
|
|
223
325
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
224
326
|
/**
|
|
@@ -251,6 +353,10 @@ declare function createTestApp(options?: TestAppOptions): Promise<TestApp>;
|
|
|
251
353
|
* "booking", "inventory"). The platform parses the `workflow:` prefix out of
|
|
252
354
|
* test reporter output to show per-workflow pass/fail on deployments — use
|
|
253
355
|
* one block per workflow the app's owner cares about.
|
|
356
|
+
*
|
|
357
|
+
* The dashboard lists the workflow's test names beneath it, so title each test
|
|
358
|
+
* as a sentence about what a person does ("someone pays with a credit card"),
|
|
359
|
+
* not about the code under it.
|
|
254
360
|
*/
|
|
255
361
|
declare function describeWorkflow(name: string, fn: () => void): void;
|
|
256
362
|
declare const WORKFLOW_NAME_PREFIX = "workflow:";
|
|
@@ -273,4 +379,4 @@ declare const TEST_ENV_DEFAULTS: {
|
|
|
273
379
|
/** Default location of the DDL snapshot written by `generate-types`. */
|
|
274
380
|
declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
275
381
|
|
|
276
|
-
export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedUpload, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
|
382
|
+
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
package/dist/index.js
CHANGED
|
@@ -110,6 +110,13 @@ var state = globalSingleton("state", () => ({
|
|
|
110
110
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
111
111
|
presignPending: /* @__PURE__ */ new Map(),
|
|
112
112
|
messages: [],
|
|
113
|
+
edgePrints: [],
|
|
114
|
+
edgeDisplays: [],
|
|
115
|
+
edgeTestPrints: [],
|
|
116
|
+
edgePrintCounter: 0,
|
|
117
|
+
edgeDevices: [],
|
|
118
|
+
edgePeripherals: [],
|
|
119
|
+
edgeBindings: /* @__PURE__ */ new Map(),
|
|
113
120
|
allowNetwork: false
|
|
114
121
|
}));
|
|
115
122
|
function requireDb() {
|
|
@@ -172,8 +179,8 @@ function verifyDeploymentAuthHeader(secret, header) {
|
|
|
172
179
|
return null;
|
|
173
180
|
}
|
|
174
181
|
if (payload.type !== "deployment-request") return null;
|
|
175
|
-
const
|
|
176
|
-
if (Math.abs(
|
|
182
|
+
const now3 = Math.floor(Date.now() / 1e3);
|
|
183
|
+
if (Math.abs(now3 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
|
|
177
184
|
return payload;
|
|
178
185
|
}
|
|
179
186
|
|
|
@@ -1475,6 +1482,224 @@ function createMessages() {
|
|
|
1475
1482
|
};
|
|
1476
1483
|
}
|
|
1477
1484
|
|
|
1485
|
+
// src/simulator/edge.ts
|
|
1486
|
+
function edgeFailure(error, status = 400, code) {
|
|
1487
|
+
return json({ success: false, error, ...code ? { code } : {} }, status);
|
|
1488
|
+
}
|
|
1489
|
+
function bindingNotFound(alias) {
|
|
1490
|
+
return edgeFailure(`No peripheral is paired to alias "${alias}"`, 404, "BINDING_NOT_FOUND");
|
|
1491
|
+
}
|
|
1492
|
+
function now2() {
|
|
1493
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1494
|
+
}
|
|
1495
|
+
function nextJobId() {
|
|
1496
|
+
state.edgePrintCounter += 1;
|
|
1497
|
+
return `job_${state.edgePrintCounter}`;
|
|
1498
|
+
}
|
|
1499
|
+
function nextConfigVersion() {
|
|
1500
|
+
return state.edgeDisplays.length + 1;
|
|
1501
|
+
}
|
|
1502
|
+
function findPeripheral(id) {
|
|
1503
|
+
return state.edgePeripherals.find((p) => p.id === id);
|
|
1504
|
+
}
|
|
1505
|
+
function buildBinding(alias, peripheralId) {
|
|
1506
|
+
const peripheral = findPeripheral(peripheralId);
|
|
1507
|
+
return {
|
|
1508
|
+
alias,
|
|
1509
|
+
state: peripheral ? "ok" : "peripheral_missing",
|
|
1510
|
+
peripheral: peripheral ? {
|
|
1511
|
+
id: peripheral.id,
|
|
1512
|
+
displayName: peripheral.displayName,
|
|
1513
|
+
driver: peripheral.driver,
|
|
1514
|
+
connected: peripheral.connected
|
|
1515
|
+
} : null,
|
|
1516
|
+
device: peripheral ? {
|
|
1517
|
+
id: peripheral.device.id,
|
|
1518
|
+
displayName: peripheral.device.displayName,
|
|
1519
|
+
status: peripheral.device.status
|
|
1520
|
+
} : null,
|
|
1521
|
+
updatedAt: now2()
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
function handleListDevices() {
|
|
1525
|
+
return success({ devices: [...state.edgeDevices] });
|
|
1526
|
+
}
|
|
1527
|
+
function handleListPeripherals(request) {
|
|
1528
|
+
const deviceId = new URL(request.url).searchParams.get("deviceId");
|
|
1529
|
+
const peripherals = deviceId ? state.edgePeripherals.filter((p) => p.device.id === deviceId) : [...state.edgePeripherals];
|
|
1530
|
+
return success({ peripherals });
|
|
1531
|
+
}
|
|
1532
|
+
function handleListBindings() {
|
|
1533
|
+
return success({ bindings: [...state.edgeBindings.values()] });
|
|
1534
|
+
}
|
|
1535
|
+
function handleGetBinding(alias) {
|
|
1536
|
+
const binding = state.edgeBindings.get(alias);
|
|
1537
|
+
if (!binding) return bindingNotFound(alias);
|
|
1538
|
+
return success({ binding });
|
|
1539
|
+
}
|
|
1540
|
+
async function handlePair(request) {
|
|
1541
|
+
const body = await readJsonBody(request);
|
|
1542
|
+
const alias = String(body.alias ?? "");
|
|
1543
|
+
const peripheralId = String(body.peripheralId ?? "");
|
|
1544
|
+
if (!alias) return edgeFailure("alias is required");
|
|
1545
|
+
if (!peripheralId) return edgeFailure("peripheralId is required");
|
|
1546
|
+
if (!findPeripheral(peripheralId)) {
|
|
1547
|
+
return edgeFailure("Peripheral not found or its device is not granted to this project", 404);
|
|
1548
|
+
}
|
|
1549
|
+
const binding = buildBinding(alias, peripheralId);
|
|
1550
|
+
state.edgeBindings.set(alias, binding);
|
|
1551
|
+
return success({ binding });
|
|
1552
|
+
}
|
|
1553
|
+
function handleUnpair(alias) {
|
|
1554
|
+
const existed = state.edgeBindings.delete(alias);
|
|
1555
|
+
if (!existed) return bindingNotFound(alias);
|
|
1556
|
+
return success({ deleted: true });
|
|
1557
|
+
}
|
|
1558
|
+
async function handlePrint(request) {
|
|
1559
|
+
const body = await readJsonBody(request);
|
|
1560
|
+
const jobId = nextJobId();
|
|
1561
|
+
const captured = {
|
|
1562
|
+
jobId,
|
|
1563
|
+
deploymentId: String(body.deploymentId ?? ""),
|
|
1564
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
1565
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
1566
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
1567
|
+
receipt: body.receipt ?? {},
|
|
1568
|
+
openDrawer: body.openDrawer === true,
|
|
1569
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0,
|
|
1570
|
+
copies: typeof body.copies === "number" ? body.copies : 1
|
|
1571
|
+
};
|
|
1572
|
+
state.edgePrints.push(captured);
|
|
1573
|
+
return success({
|
|
1574
|
+
jobId,
|
|
1575
|
+
status: "completed"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
async function handleShowDisplay(request) {
|
|
1579
|
+
const body = await readJsonBody(request);
|
|
1580
|
+
const captured = {
|
|
1581
|
+
action: "show",
|
|
1582
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
1583
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
1584
|
+
url: body.url ? String(body.url) : void 0
|
|
1585
|
+
};
|
|
1586
|
+
state.edgeDisplays.push(captured);
|
|
1587
|
+
return success({
|
|
1588
|
+
configVersion: nextConfigVersion(),
|
|
1589
|
+
pushed: true,
|
|
1590
|
+
state: "showing"
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
function handleClearDisplay(request) {
|
|
1594
|
+
const url = new URL(request.url);
|
|
1595
|
+
const alias = url.searchParams.get("alias");
|
|
1596
|
+
const peripheralId = url.searchParams.get("peripheralId");
|
|
1597
|
+
const captured = {
|
|
1598
|
+
action: "clear",
|
|
1599
|
+
alias: alias ?? void 0,
|
|
1600
|
+
peripheralId: peripheralId ?? void 0
|
|
1601
|
+
};
|
|
1602
|
+
state.edgeDisplays.push(captured);
|
|
1603
|
+
return success({
|
|
1604
|
+
configVersion: nextConfigVersion(),
|
|
1605
|
+
pushed: true,
|
|
1606
|
+
state: "cleared"
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
async function handleTestPrint(request) {
|
|
1610
|
+
const body = await readJsonBody(request);
|
|
1611
|
+
const captured = {
|
|
1612
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
1613
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
1614
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0
|
|
1615
|
+
};
|
|
1616
|
+
state.edgeTestPrints.push(captured);
|
|
1617
|
+
return success({
|
|
1618
|
+
status: "ok",
|
|
1619
|
+
peripheralId: captured.peripheralId ?? captured.alias ?? "default"
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
async function handleEdgeRequest(request, subPath) {
|
|
1623
|
+
const method = request.method;
|
|
1624
|
+
if (subPath === "/print" && method === "POST") {
|
|
1625
|
+
return handlePrint(request);
|
|
1626
|
+
}
|
|
1627
|
+
if (subPath === "/devices" && method === "GET") {
|
|
1628
|
+
return handleListDevices();
|
|
1629
|
+
}
|
|
1630
|
+
if (subPath === "/peripherals" && method === "GET") {
|
|
1631
|
+
return handleListPeripherals(request);
|
|
1632
|
+
}
|
|
1633
|
+
if (subPath === "/bindings" && method === "GET") {
|
|
1634
|
+
return handleListBindings();
|
|
1635
|
+
}
|
|
1636
|
+
if (subPath === "/bindings" && method === "PUT") {
|
|
1637
|
+
return handlePair(request);
|
|
1638
|
+
}
|
|
1639
|
+
const bindingMatch = subPath.match(/^\/bindings\/([^/]+)$/);
|
|
1640
|
+
if (bindingMatch) {
|
|
1641
|
+
const alias = decodeURIComponent(bindingMatch[1]);
|
|
1642
|
+
if (method === "GET") return handleGetBinding(alias);
|
|
1643
|
+
if (method === "DELETE") return handleUnpair(alias);
|
|
1644
|
+
}
|
|
1645
|
+
if (subPath === "/display" && method === "POST") {
|
|
1646
|
+
return handleShowDisplay(request);
|
|
1647
|
+
}
|
|
1648
|
+
if (subPath === "/display" && method === "DELETE") {
|
|
1649
|
+
return handleClearDisplay(request);
|
|
1650
|
+
}
|
|
1651
|
+
if (subPath === "/test-print" && method === "POST") {
|
|
1652
|
+
return handleTestPrint(request);
|
|
1653
|
+
}
|
|
1654
|
+
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
1655
|
+
}
|
|
1656
|
+
function createEdge() {
|
|
1657
|
+
return {
|
|
1658
|
+
get prints() {
|
|
1659
|
+
return [...state.edgePrints];
|
|
1660
|
+
},
|
|
1661
|
+
get displays() {
|
|
1662
|
+
return [...state.edgeDisplays];
|
|
1663
|
+
},
|
|
1664
|
+
get testPrints() {
|
|
1665
|
+
return [...state.edgeTestPrints];
|
|
1666
|
+
},
|
|
1667
|
+
latestPrint() {
|
|
1668
|
+
return state.edgePrints[state.edgePrints.length - 1];
|
|
1669
|
+
},
|
|
1670
|
+
latestDisplay() {
|
|
1671
|
+
return state.edgeDisplays[state.edgeDisplays.length - 1];
|
|
1672
|
+
},
|
|
1673
|
+
get bindings() {
|
|
1674
|
+
return [...state.edgeBindings.values()];
|
|
1675
|
+
},
|
|
1676
|
+
seedDevices(devices) {
|
|
1677
|
+
state.edgeDevices = [...devices];
|
|
1678
|
+
},
|
|
1679
|
+
seedPeripherals(peripherals) {
|
|
1680
|
+
state.edgePeripherals = [...peripherals];
|
|
1681
|
+
},
|
|
1682
|
+
seedBindings(bindings) {
|
|
1683
|
+
state.edgeBindings.clear();
|
|
1684
|
+
for (const binding of bindings) {
|
|
1685
|
+
state.edgeBindings.set(binding.alias, binding);
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
clear() {
|
|
1689
|
+
state.edgePrints = [];
|
|
1690
|
+
state.edgeDisplays = [];
|
|
1691
|
+
state.edgeTestPrints = [];
|
|
1692
|
+
state.edgePrintCounter = 0;
|
|
1693
|
+
state.edgeDevices = [];
|
|
1694
|
+
state.edgePeripherals = [];
|
|
1695
|
+
state.edgeBindings.clear();
|
|
1696
|
+
},
|
|
1697
|
+
get count() {
|
|
1698
|
+
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1478
1703
|
// src/simulator/router.ts
|
|
1479
1704
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
1480
1705
|
originalFetch: null
|
|
@@ -1495,7 +1720,8 @@ function requiresDeploymentHmac(request, url) {
|
|
|
1495
1720
|
const messagingMatch = url.pathname.match(
|
|
1496
1721
|
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
1497
1722
|
);
|
|
1498
|
-
|
|
1723
|
+
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
1724
|
+
return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
1499
1725
|
}
|
|
1500
1726
|
async function handleSimulatedRequest(request, url) {
|
|
1501
1727
|
if (url.pathname === "/sql") {
|
|
@@ -1512,6 +1738,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
1512
1738
|
const messagingMatch = url.pathname.match(
|
|
1513
1739
|
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
1514
1740
|
);
|
|
1741
|
+
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
1515
1742
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
1516
1743
|
if (requiresDeploymentHmac(request, url)) {
|
|
1517
1744
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
@@ -1539,6 +1766,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
1539
1766
|
const channel = messagingMatch[1];
|
|
1540
1767
|
return handleMessagingRequest(request, channel, messagingMatch[2] ?? "");
|
|
1541
1768
|
}
|
|
1769
|
+
if (edgeMatch) {
|
|
1770
|
+
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
1771
|
+
}
|
|
1542
1772
|
if (dataStoreMatch) {
|
|
1543
1773
|
const subPath = dataStoreMatch[1] ?? "";
|
|
1544
1774
|
const db = requireDb();
|
|
@@ -1560,7 +1790,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
1560
1790
|
}
|
|
1561
1791
|
}
|
|
1562
1792
|
return failure(
|
|
1563
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, auth verify/refresh, Neon /sql.`,
|
|
1793
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
|
|
1564
1794
|
404
|
|
1565
1795
|
);
|
|
1566
1796
|
}
|
|
@@ -1649,6 +1879,7 @@ async function createTestApp(options = {}) {
|
|
|
1649
1879
|
const payments = createPayments();
|
|
1650
1880
|
const storage = createStorage();
|
|
1651
1881
|
const messages = createMessages();
|
|
1882
|
+
const edge = createEdge();
|
|
1652
1883
|
const app = {
|
|
1653
1884
|
db,
|
|
1654
1885
|
inbox,
|
|
@@ -1656,6 +1887,7 @@ async function createTestApp(options = {}) {
|
|
|
1656
1887
|
payments,
|
|
1657
1888
|
storage,
|
|
1658
1889
|
messages,
|
|
1890
|
+
edge,
|
|
1659
1891
|
async query(sql, params = []) {
|
|
1660
1892
|
const result = await db.query(sql, params);
|
|
1661
1893
|
return result.rows;
|
|
@@ -1691,6 +1923,7 @@ async function createTestApp(options = {}) {
|
|
|
1691
1923
|
payments.clear();
|
|
1692
1924
|
storage.clear();
|
|
1693
1925
|
messages.clear();
|
|
1926
|
+
edge.clear();
|
|
1694
1927
|
},
|
|
1695
1928
|
async close() {
|
|
1696
1929
|
state.db = null;
|
|
@@ -1704,6 +1937,7 @@ async function createTestApp(options = {}) {
|
|
|
1704
1937
|
payments.clear();
|
|
1705
1938
|
storage.clear();
|
|
1706
1939
|
messages.clear();
|
|
1940
|
+
edge.clear();
|
|
1707
1941
|
uninstallFetchRouter();
|
|
1708
1942
|
await db.close();
|
|
1709
1943
|
}
|