@stardeck-customer-apps/testing 0.10.0 → 0.11.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/SKILL.md CHANGED
@@ -203,6 +203,26 @@ const integrations = createIntegrationsClient();
203
203
  await integrations.line.push("U123", { type: "text", text: "Your order shipped" });
204
204
  expect(app.messages.channel("line").to("U123")[0].body.text).toMatch(/shipped/i);
205
205
 
206
+ // LINE Flex card — the simulator preserves the complete LINE-native container.
207
+ const card = {
208
+ type: "bubble" as const,
209
+ body: {
210
+ type: "box",
211
+ layout: "vertical",
212
+ contents: [{ type: "text", text: "Order shipped" }],
213
+ },
214
+ };
215
+ await integrations.line.push("U123", {
216
+ type: "flex",
217
+ altText: "Your order shipped",
218
+ contents: card,
219
+ });
220
+ expect(app.messages.channel("line").to("U123")[1].body).toEqual({
221
+ type: "flex",
222
+ altText: "Your order shipped",
223
+ contents: card,
224
+ });
225
+
206
226
  // Guest/counter identity resolution. `provenance` is selected by this trusted
207
227
  // server route; never copy it from a browser form or request body.
208
228
  const identity = await integrations.identities.resolveOrCreate({
@@ -288,7 +308,10 @@ externalId })` helper models a trusted platform/channel link, and
288
308
  list `limit` 1–100, status filter values).
289
309
  - `app.storage` — uploads through storage-sdk: `.uploads`, `.latest()`, `.count`, `.clear()`.
290
310
  - `app.messages` — outbound Slack/LINE/Facebook sends:
291
- `.all()`, `.latest()`, `.to(recipient)`, `.channel("line")`, `.count`, `.clear()`.
311
+ `.all()`, `.latest()`, `.to(recipient)`, `.channel("line")`, `.count`, `.clear()`. LINE Flex
312
+ captures expose `body.type`, `body.altText`, and the complete `body.contents` bubble/carousel.
313
+ The simulator validates the Flex envelope like production; LINE itself remains responsible for
314
+ validating nested component layout.
292
315
  - `app.edge` — edge print/display/bindings from edge-sdk: `.prints`, `.displays`,
293
316
  `.testPrints`, `.latestPrint()`, `.latestDisplay()`, `.bindings`,
294
317
  `.seedDevices()`, `.seedPeripherals()`, `.seedBindings()`,
package/dist/index.d.mts CHANGED
@@ -252,6 +252,7 @@ interface CapturedMessage {
252
252
  channel: "slack" | "line" | "facebook";
253
253
  recipient: string;
254
254
  body: {
255
+ type?: string;
255
256
  text?: string;
256
257
  blocks?: unknown[];
257
258
  threadTs?: string;
@@ -261,6 +262,10 @@ interface CapturedMessage {
261
262
  originalContentUrl?: string;
262
263
  /** LINE image push — HTTPS URL of the preview; may be omitted when not sent. */
263
264
  previewImageUrl?: string;
265
+ /** LINE Flex fallback text shown when the rich card cannot be rendered. */
266
+ altText?: string;
267
+ /** Complete LINE Flex bubble or carousel payload. */
268
+ contents?: Record<string, unknown>;
264
269
  };
265
270
  connectionId?: string;
266
271
  sentAt: Date;
package/dist/index.d.ts CHANGED
@@ -252,6 +252,7 @@ interface CapturedMessage {
252
252
  channel: "slack" | "line" | "facebook";
253
253
  recipient: string;
254
254
  body: {
255
+ type?: string;
255
256
  text?: string;
256
257
  blocks?: unknown[];
257
258
  threadTs?: string;
@@ -261,6 +262,10 @@ interface CapturedMessage {
261
262
  originalContentUrl?: string;
262
263
  /** LINE image push — HTTPS URL of the preview; may be omitted when not sent. */
263
264
  previewImageUrl?: string;
265
+ /** LINE Flex fallback text shown when the rich card cannot be rendered. */
266
+ altText?: string;
267
+ /** Complete LINE Flex bubble or carousel payload. */
268
+ contents?: Record<string, unknown>;
264
269
  };
265
270
  connectionId?: string;
266
271
  sentAt: Date;
package/dist/index.js CHANGED
@@ -2072,6 +2072,9 @@ function isValidLineImageUrl(url) {
2072
2072
  return false;
2073
2073
  }
2074
2074
  }
2075
+ function isObject(value) {
2076
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2077
+ }
2075
2078
  function captureMessage(channel, recipient, body, connectionId) {
2076
2079
  const message = {
2077
2080
  channel,
@@ -2083,7 +2086,8 @@ function captureMessage(channel, recipient, body, connectionId) {
2083
2086
  messagingType: body.messagingType ? String(body.messagingType) : void 0,
2084
2087
  tag: body.tag ? String(body.tag) : void 0,
2085
2088
  originalContentUrl: body.originalContentUrl ? String(body.originalContentUrl) : void 0,
2086
- previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0
2089
+ previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0,
2090
+ ...body.type === "flex" && isObject(body.contents) ? { type: "flex", altText: String(body.altText), contents: body.contents } : {}
2087
2091
  },
2088
2092
  connectionId,
2089
2093
  sentAt: /* @__PURE__ */ new Date()
@@ -2121,8 +2125,8 @@ async function handleMessagingRequest(request, channel, subPath) {
2121
2125
  const userId = String(body.userId ?? "");
2122
2126
  const message = body.message;
2123
2127
  if (!userId) return failure("userId is required");
2124
- if (!message || message.type !== "text" && message.type !== "image") {
2125
- return failure("message must be { type: 'text' | 'image', ... }");
2128
+ if (!message || message.type !== "text" && message.type !== "image" && message.type !== "flex") {
2129
+ return failure("message must be { type: 'text' | 'image' | 'flex', ... }");
2126
2130
  }
2127
2131
  const connectionId = body.connectionId ? String(body.connectionId) : void 0;
2128
2132
  if (message.type === "text") {
@@ -2133,7 +2137,7 @@ async function handleMessagingRequest(request, channel, subPath) {
2133
2137
  return failure("message text exceeds maximum length");
2134
2138
  }
2135
2139
  captureMessage("line", userId, { text: message.text }, connectionId);
2136
- } else {
2140
+ } else if (message.type === "image") {
2137
2141
  const originalContentUrl = message.originalContentUrl;
2138
2142
  if (!originalContentUrl || !isValidLineImageUrl(originalContentUrl)) {
2139
2143
  return failure("originalContentUrl must be a valid HTTPS URL (max 2000 chars)");
@@ -2144,7 +2148,23 @@ async function handleMessagingRequest(request, channel, subPath) {
2144
2148
  captureMessage(
2145
2149
  "line",
2146
2150
  userId,
2147
- { originalContentUrl, previewImageUrl: message.previewImageUrl ?? originalContentUrl },
2151
+ {
2152
+ originalContentUrl,
2153
+ previewImageUrl: message.previewImageUrl ?? originalContentUrl
2154
+ },
2155
+ connectionId
2156
+ );
2157
+ } else {
2158
+ if (typeof message.altText !== "string" || !message.altText || message.altText.length > 400) {
2159
+ return failure("altText must be between 1 and 400 characters");
2160
+ }
2161
+ if (!isObject(message.contents) || message.contents.type !== "bubble" && message.contents.type !== "carousel") {
2162
+ return failure("contents.type must be 'bubble' or 'carousel'");
2163
+ }
2164
+ captureMessage(
2165
+ "line",
2166
+ userId,
2167
+ { type: "flex", altText: message.altText, contents: message.contents },
2148
2168
  connectionId
2149
2169
  );
2150
2170
  }
package/dist/index.mjs CHANGED
@@ -2023,6 +2023,9 @@ function isValidLineImageUrl(url) {
2023
2023
  return false;
2024
2024
  }
2025
2025
  }
2026
+ function isObject(value) {
2027
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2028
+ }
2026
2029
  function captureMessage(channel, recipient, body, connectionId) {
2027
2030
  const message = {
2028
2031
  channel,
@@ -2034,7 +2037,8 @@ function captureMessage(channel, recipient, body, connectionId) {
2034
2037
  messagingType: body.messagingType ? String(body.messagingType) : void 0,
2035
2038
  tag: body.tag ? String(body.tag) : void 0,
2036
2039
  originalContentUrl: body.originalContentUrl ? String(body.originalContentUrl) : void 0,
2037
- previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0
2040
+ previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0,
2041
+ ...body.type === "flex" && isObject(body.contents) ? { type: "flex", altText: String(body.altText), contents: body.contents } : {}
2038
2042
  },
2039
2043
  connectionId,
2040
2044
  sentAt: /* @__PURE__ */ new Date()
@@ -2072,8 +2076,8 @@ async function handleMessagingRequest(request, channel, subPath) {
2072
2076
  const userId = String(body.userId ?? "");
2073
2077
  const message = body.message;
2074
2078
  if (!userId) return failure("userId is required");
2075
- if (!message || message.type !== "text" && message.type !== "image") {
2076
- return failure("message must be { type: 'text' | 'image', ... }");
2079
+ if (!message || message.type !== "text" && message.type !== "image" && message.type !== "flex") {
2080
+ return failure("message must be { type: 'text' | 'image' | 'flex', ... }");
2077
2081
  }
2078
2082
  const connectionId = body.connectionId ? String(body.connectionId) : void 0;
2079
2083
  if (message.type === "text") {
@@ -2084,7 +2088,7 @@ async function handleMessagingRequest(request, channel, subPath) {
2084
2088
  return failure("message text exceeds maximum length");
2085
2089
  }
2086
2090
  captureMessage("line", userId, { text: message.text }, connectionId);
2087
- } else {
2091
+ } else if (message.type === "image") {
2088
2092
  const originalContentUrl = message.originalContentUrl;
2089
2093
  if (!originalContentUrl || !isValidLineImageUrl(originalContentUrl)) {
2090
2094
  return failure("originalContentUrl must be a valid HTTPS URL (max 2000 chars)");
@@ -2095,7 +2099,23 @@ async function handleMessagingRequest(request, channel, subPath) {
2095
2099
  captureMessage(
2096
2100
  "line",
2097
2101
  userId,
2098
- { originalContentUrl, previewImageUrl: message.previewImageUrl ?? originalContentUrl },
2102
+ {
2103
+ originalContentUrl,
2104
+ previewImageUrl: message.previewImageUrl ?? originalContentUrl
2105
+ },
2106
+ connectionId
2107
+ );
2108
+ } else {
2109
+ if (typeof message.altText !== "string" || !message.altText || message.altText.length > 400) {
2110
+ return failure("altText must be between 1 and 400 characters");
2111
+ }
2112
+ if (!isObject(message.contents) || message.contents.type !== "bubble" && message.contents.type !== "carousel") {
2113
+ return failure("contents.type must be 'bubble' or 'carousel'");
2114
+ }
2115
+ captureMessage(
2116
+ "line",
2117
+ userId,
2118
+ { type: "flex", altText: message.altText, contents: message.contents },
2099
2119
  connectionId
2100
2120
  );
2101
2121
  }
package/dist/setup.js CHANGED
@@ -1622,6 +1622,9 @@ function isValidLineImageUrl(url) {
1622
1622
  return false;
1623
1623
  }
1624
1624
  }
1625
+ function isObject(value) {
1626
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1627
+ }
1625
1628
  function captureMessage(channel, recipient, body, connectionId) {
1626
1629
  const message = {
1627
1630
  channel,
@@ -1633,7 +1636,8 @@ function captureMessage(channel, recipient, body, connectionId) {
1633
1636
  messagingType: body.messagingType ? String(body.messagingType) : void 0,
1634
1637
  tag: body.tag ? String(body.tag) : void 0,
1635
1638
  originalContentUrl: body.originalContentUrl ? String(body.originalContentUrl) : void 0,
1636
- previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0
1639
+ previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0,
1640
+ ...body.type === "flex" && isObject(body.contents) ? { type: "flex", altText: String(body.altText), contents: body.contents } : {}
1637
1641
  },
1638
1642
  connectionId,
1639
1643
  sentAt: /* @__PURE__ */ new Date()
@@ -1671,8 +1675,8 @@ async function handleMessagingRequest(request, channel, subPath) {
1671
1675
  const userId = String(body.userId ?? "");
1672
1676
  const message = body.message;
1673
1677
  if (!userId) return failure("userId is required");
1674
- if (!message || message.type !== "text" && message.type !== "image") {
1675
- return failure("message must be { type: 'text' | 'image', ... }");
1678
+ if (!message || message.type !== "text" && message.type !== "image" && message.type !== "flex") {
1679
+ return failure("message must be { type: 'text' | 'image' | 'flex', ... }");
1676
1680
  }
1677
1681
  const connectionId = body.connectionId ? String(body.connectionId) : void 0;
1678
1682
  if (message.type === "text") {
@@ -1683,7 +1687,7 @@ async function handleMessagingRequest(request, channel, subPath) {
1683
1687
  return failure("message text exceeds maximum length");
1684
1688
  }
1685
1689
  captureMessage("line", userId, { text: message.text }, connectionId);
1686
- } else {
1690
+ } else if (message.type === "image") {
1687
1691
  const originalContentUrl = message.originalContentUrl;
1688
1692
  if (!originalContentUrl || !isValidLineImageUrl(originalContentUrl)) {
1689
1693
  return failure("originalContentUrl must be a valid HTTPS URL (max 2000 chars)");
@@ -1694,7 +1698,23 @@ async function handleMessagingRequest(request, channel, subPath) {
1694
1698
  captureMessage(
1695
1699
  "line",
1696
1700
  userId,
1697
- { originalContentUrl, previewImageUrl: message.previewImageUrl ?? originalContentUrl },
1701
+ {
1702
+ originalContentUrl,
1703
+ previewImageUrl: message.previewImageUrl ?? originalContentUrl
1704
+ },
1705
+ connectionId
1706
+ );
1707
+ } else {
1708
+ if (typeof message.altText !== "string" || !message.altText || message.altText.length > 400) {
1709
+ return failure("altText must be between 1 and 400 characters");
1710
+ }
1711
+ if (!isObject(message.contents) || message.contents.type !== "bubble" && message.contents.type !== "carousel") {
1712
+ return failure("contents.type must be 'bubble' or 'carousel'");
1713
+ }
1714
+ captureMessage(
1715
+ "line",
1716
+ userId,
1717
+ { type: "flex", altText: message.altText, contents: message.contents },
1698
1718
  connectionId
1699
1719
  );
1700
1720
  }
package/dist/setup.mjs CHANGED
@@ -1598,6 +1598,9 @@ function isValidLineImageUrl(url) {
1598
1598
  return false;
1599
1599
  }
1600
1600
  }
1601
+ function isObject(value) {
1602
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1603
+ }
1601
1604
  function captureMessage(channel, recipient, body, connectionId) {
1602
1605
  const message = {
1603
1606
  channel,
@@ -1609,7 +1612,8 @@ function captureMessage(channel, recipient, body, connectionId) {
1609
1612
  messagingType: body.messagingType ? String(body.messagingType) : void 0,
1610
1613
  tag: body.tag ? String(body.tag) : void 0,
1611
1614
  originalContentUrl: body.originalContentUrl ? String(body.originalContentUrl) : void 0,
1612
- previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0
1615
+ previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0,
1616
+ ...body.type === "flex" && isObject(body.contents) ? { type: "flex", altText: String(body.altText), contents: body.contents } : {}
1613
1617
  },
1614
1618
  connectionId,
1615
1619
  sentAt: /* @__PURE__ */ new Date()
@@ -1647,8 +1651,8 @@ async function handleMessagingRequest(request, channel, subPath) {
1647
1651
  const userId = String(body.userId ?? "");
1648
1652
  const message = body.message;
1649
1653
  if (!userId) return failure("userId is required");
1650
- if (!message || message.type !== "text" && message.type !== "image") {
1651
- return failure("message must be { type: 'text' | 'image', ... }");
1654
+ if (!message || message.type !== "text" && message.type !== "image" && message.type !== "flex") {
1655
+ return failure("message must be { type: 'text' | 'image' | 'flex', ... }");
1652
1656
  }
1653
1657
  const connectionId = body.connectionId ? String(body.connectionId) : void 0;
1654
1658
  if (message.type === "text") {
@@ -1659,7 +1663,7 @@ async function handleMessagingRequest(request, channel, subPath) {
1659
1663
  return failure("message text exceeds maximum length");
1660
1664
  }
1661
1665
  captureMessage("line", userId, { text: message.text }, connectionId);
1662
- } else {
1666
+ } else if (message.type === "image") {
1663
1667
  const originalContentUrl = message.originalContentUrl;
1664
1668
  if (!originalContentUrl || !isValidLineImageUrl(originalContentUrl)) {
1665
1669
  return failure("originalContentUrl must be a valid HTTPS URL (max 2000 chars)");
@@ -1670,7 +1674,23 @@ async function handleMessagingRequest(request, channel, subPath) {
1670
1674
  captureMessage(
1671
1675
  "line",
1672
1676
  userId,
1673
- { originalContentUrl, previewImageUrl: message.previewImageUrl ?? originalContentUrl },
1677
+ {
1678
+ originalContentUrl,
1679
+ previewImageUrl: message.previewImageUrl ?? originalContentUrl
1680
+ },
1681
+ connectionId
1682
+ );
1683
+ } else {
1684
+ if (typeof message.altText !== "string" || !message.altText || message.altText.length > 400) {
1685
+ return failure("altText must be between 1 and 400 characters");
1686
+ }
1687
+ if (!isObject(message.contents) || message.contents.type !== "bubble" && message.contents.type !== "carousel") {
1688
+ return failure("contents.type must be 'bubble' or 'carousel'");
1689
+ }
1690
+ captureMessage(
1691
+ "line",
1692
+ userId,
1693
+ { type: "flex", altText: message.altText, contents: message.contents },
1674
1694
  connectionId
1675
1695
  );
1676
1696
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@stardeck-customer-apps/edge-sdk": ">=1.4.0",
69
- "@stardeck-customer-apps/integrations-sdk": ">=1.6.0",
69
+ "@stardeck-customer-apps/integrations-sdk": ">=1.13.0",
70
70
  "@stardeck-customer-apps/payments-sdk": ">=0.1.0",
71
71
  "@stardeck-customer-apps/storage-sdk": ">=0.1.0",
72
72
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",