@navipartner/demo-video 0.0.0-stage → 0.4.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/README.md +109 -3
- package/bin/args.d.mts +30 -0
- package/bin/args.mjs +153 -0
- package/bin/demo-video.mjs +272 -0
- package/bin/encode.mjs +187 -0
- package/bin/init.d.mts +24 -0
- package/bin/init.mjs +447 -0
- package/bin/pick.d.mts +18 -0
- package/bin/pick.mjs +252 -0
- package/bin/runner.d.mts +5 -0
- package/bin/runner.mjs +47 -0
- package/brands/navipartner/logo.svg +19 -0
- package/brands/navipartner.json +22 -0
- package/dist/adapter-CWqMoNdw.d.ts +157 -0
- package/dist/chunk-BYAFDWPA.js +768 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +56 -0
- package/dist/runner/index.d.ts +23 -0
- package/dist/runner/index.js +1258 -0
- package/package.json +67 -3
- package/skills/np-demo-video/SKILL.md +348 -0
- package/skills/np-demo-video/look.md +158 -0
- package/skills/np-demo-video/reference.md +82 -0
package/bin/pick.mjs
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { listStories } from "./args.mjs";
|
|
6
|
+
|
|
7
|
+
// `demo-video pick`: turns a `/demo-video …` request into one of the
|
|
8
|
+
// already-made stories and a brand. No AI and nothing written: stories are a
|
|
9
|
+
// developer's to make (locally, with the skill); a request only chooses.
|
|
10
|
+
//
|
|
11
|
+
// /demo-video → the primary story (one app)
|
|
12
|
+
// /demo-video checkout in acme branding → story "checkout", brand "acme"
|
|
13
|
+
// /demo-video retail checkout → the app whose folder says retail
|
|
14
|
+
//
|
|
15
|
+
// Anything it can't settle on one story is answered with the menu.
|
|
16
|
+
|
|
17
|
+
const engineDir = path.resolve(
|
|
18
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
19
|
+
"..",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const STOP = new Set(
|
|
23
|
+
"a an and as at by for from in into make me of on or our please show story the this to video with branding brand marketing demo film render".split(
|
|
24
|
+
" ",
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const words = (text) =>
|
|
29
|
+
(text.toLowerCase().match(/[a-z0-9æøå]+(?:-[a-z0-9æøå]+)*/g) ?? []).flatMap(
|
|
30
|
+
(w) => [w, ...(w.includes("-") ? w.split("-") : [])],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const brandNames = (dir) =>
|
|
34
|
+
fs.existsSync(dir)
|
|
35
|
+
? fs
|
|
36
|
+
.readdirSync(dir)
|
|
37
|
+
.filter((f) => f.endsWith(".json"))
|
|
38
|
+
.map((f) => f.slice(0, -".json".length))
|
|
39
|
+
: [];
|
|
40
|
+
|
|
41
|
+
// The adapter may keep its brands elsewhere (`brands: "../brands"`). Read
|
|
42
|
+
// from demo.config.ts's text: a literal path, as the field is documented.
|
|
43
|
+
const adapterBrandsDir = (appDir) => {
|
|
44
|
+
const config = path.join(appDir, "demo", "demo.config.ts");
|
|
45
|
+
const text = fs.existsSync(config) ? fs.readFileSync(config, "utf8") : "";
|
|
46
|
+
const dir = /\bbrands:\s*["'`]([^"'`]+)["'`]/.exec(text)?.[1] ?? "./brands";
|
|
47
|
+
return path.resolve(appDir, "demo", dir);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** What an app offers: its stories, and its brands plus the engine's. */
|
|
51
|
+
export const appMenu = (appDir) => ({
|
|
52
|
+
dir: appDir,
|
|
53
|
+
name: path.basename(appDir),
|
|
54
|
+
stories: listStories(path.join(appDir, "demo")),
|
|
55
|
+
brands: [
|
|
56
|
+
...new Set([
|
|
57
|
+
...brandNames(adapterBrandsDir(appDir)),
|
|
58
|
+
...brandNames(path.join(engineDir, "brands")),
|
|
59
|
+
]),
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Picks from `apps` (appMenu results). Returns { app, story, brand } (brand
|
|
65
|
+
* null = the adapter's default) or { menu: true, reason }.
|
|
66
|
+
*/
|
|
67
|
+
export const pick = (request, apps) => {
|
|
68
|
+
const said = words(request);
|
|
69
|
+
const has = (word) => said.includes(word.toLowerCase());
|
|
70
|
+
|
|
71
|
+
// The brand: any offered brand named in the request.
|
|
72
|
+
const allBrands = [...new Set(apps.flatMap((a) => a.brands))];
|
|
73
|
+
const brands = allBrands.filter(has);
|
|
74
|
+
if (brands.length > 1) {
|
|
75
|
+
return {
|
|
76
|
+
menu: true,
|
|
77
|
+
reason: `Name one brand, not ${brands.join(" and ")}.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const brand = brands[0] ?? null;
|
|
81
|
+
|
|
82
|
+
// What's left of the request once the brand and filler are gone.
|
|
83
|
+
const content = said.filter(
|
|
84
|
+
(w) => !STOP.has(w) && !brands.includes(w) && !/^\d+$/.test(w),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// The app: named by its folder, else by a part of it no other app's
|
|
88
|
+
// folder has ("retail", not "self"), else the only one.
|
|
89
|
+
let candidates = apps;
|
|
90
|
+
if (apps.length > 1) {
|
|
91
|
+
const exact = apps.filter((a) => has(a.name));
|
|
92
|
+
const partCount = (part) =>
|
|
93
|
+
apps.filter((a) => a.name.split("-").includes(part)).length;
|
|
94
|
+
const named = exact.length
|
|
95
|
+
? exact
|
|
96
|
+
: apps.filter((a) =>
|
|
97
|
+
a.name
|
|
98
|
+
.split("-")
|
|
99
|
+
.some(
|
|
100
|
+
(part) => part.length > 3 && partCount(part) === 1 && has(part),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
if (named.length === 1) {
|
|
104
|
+
candidates = named;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Score every story: its name (or a part of it) said outright counts most,
|
|
109
|
+
// then words shared with its description.
|
|
110
|
+
// Variants aren't scored: the base story plus the brand finds them.
|
|
111
|
+
const scored = candidates.flatMap((app) =>
|
|
112
|
+
app.stories
|
|
113
|
+
.filter((story) => !story.extends)
|
|
114
|
+
.map((story) => {
|
|
115
|
+
const nameParts = story.name.split("-");
|
|
116
|
+
// "checkout express" says the story checkout-express in full.
|
|
117
|
+
let score = has(story.name) || nameParts.every((p) => has(p)) ? 10 : 0;
|
|
118
|
+
score += nameParts.filter((p) => p.length > 2 && has(p)).length * 3;
|
|
119
|
+
const description = new Set(words(story.description ?? ""));
|
|
120
|
+
score += content.filter(
|
|
121
|
+
(w) => w.length > 2 && description.has(w),
|
|
122
|
+
).length;
|
|
123
|
+
return { app, story, score };
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
const best = Math.max(0, ...scored.map((s) => s.score));
|
|
127
|
+
const top = scored.filter((s) => s.score === best);
|
|
128
|
+
|
|
129
|
+
if (best === 0) {
|
|
130
|
+
// Nothing named a story: a request with no feature gets the primary
|
|
131
|
+
// story ("a marketing video of the retail app"), if that's one app.
|
|
132
|
+
const noFeature = content.every((w) =>
|
|
133
|
+
candidates.some(
|
|
134
|
+
(a) => a.name.includes(w) || a.name.split("-").includes(w),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
const primaries = candidates
|
|
138
|
+
.map((app) => ({ app, story: app.stories.find((s) => s.primary) }))
|
|
139
|
+
.filter((p) => p.story);
|
|
140
|
+
if (noFeature && candidates.length === 1 && primaries.length === 1) {
|
|
141
|
+
return withBrand(primaries[0].app, primaries[0].story.name, brand);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
menu: true,
|
|
145
|
+
reason:
|
|
146
|
+
candidates.length > 1 && noFeature
|
|
147
|
+
? "Which app?"
|
|
148
|
+
: "No story matches that. Pick one by name:",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (top.length > 1) {
|
|
152
|
+
return {
|
|
153
|
+
menu: true,
|
|
154
|
+
reason: `That matches ${top.map((t) => `\`${t.story.name}\``).join(", ")}. Pick one by name:`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return withBrand(top[0].app, top[0].story.name, brand);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// The brand must be one the chosen app has: another app's brand would fail
|
|
161
|
+
// at render time with a worse message. A story with a variant for that brand
|
|
162
|
+
// (checkout + acme → checkout.acme) films the variant: its words are the
|
|
163
|
+
// customer's.
|
|
164
|
+
const withBrand = (app, story, brand) => {
|
|
165
|
+
if (brand && !app.brands.includes(brand)) {
|
|
166
|
+
return {
|
|
167
|
+
menu: true,
|
|
168
|
+
reason: `No brand \`${brand}\` in ${app.name}. Pick one:`,
|
|
169
|
+
apps: [app],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const variant =
|
|
173
|
+
brand && app.stories.find((s) => s.extends === story && s.brand === brand);
|
|
174
|
+
return { app: app.dir, story: variant ? variant.name : story, brand };
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** The menu as Markdown, for the PR reply. */
|
|
178
|
+
export const menuMarkdown = (apps, reason) => {
|
|
179
|
+
const lines = [reason, ""];
|
|
180
|
+
for (const app of apps) {
|
|
181
|
+
lines.push(`**${app.name}**`);
|
|
182
|
+
if (app.stories.length === 0) {
|
|
183
|
+
lines.push("- no stories yet");
|
|
184
|
+
}
|
|
185
|
+
for (const s of app.stories.filter((x) => !x.extends)) {
|
|
186
|
+
lines.push(
|
|
187
|
+
`- \`${s.name}\`${s.primary ? " (primary)" : ""}${s.description ? `: ${s.description}` : ""}`,
|
|
188
|
+
);
|
|
189
|
+
for (const v of app.stories.filter((x) => x.extends === s.name)) {
|
|
190
|
+
lines.push(` - in \`${v.brand}\` branding: \`${v.name}\``);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
lines.push(`- brands: ${app.brands.map((b) => `\`${b}\``).join(", ")}`, "");
|
|
194
|
+
}
|
|
195
|
+
const example = apps.find((a) => a.stories.some((s) => !s.extends));
|
|
196
|
+
if (example) {
|
|
197
|
+
lines.push(
|
|
198
|
+
`For example: \`/demo-video ${example.stories.find((s) => !s.extends).name}${example.brands.length > 1 ? ` in ${example.brands.find((b) => b !== "navipartner") ?? example.brands[0]} branding` : ""}\``,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The form's version (Actions → Run workflow): app, story and brand by name,
|
|
206
|
+
* no guessing. Empty story = the primary story, empty brand = the adapter's
|
|
207
|
+
* default, empty app = the only app. A name that doesn't exist gets the menu,
|
|
208
|
+
* so a typo never films something else. `apps` in a menu result narrows it.
|
|
209
|
+
*/
|
|
210
|
+
export const pickExact = (apps, { app = "", story = "", brand = "" }) => {
|
|
211
|
+
const slashes = (p) => p.split("\\").join("/");
|
|
212
|
+
const wanted = slashes(app).replace(/\/+$/, "");
|
|
213
|
+
const candidates = wanted
|
|
214
|
+
? apps.filter((a) => {
|
|
215
|
+
const dir = slashes(a.dir);
|
|
216
|
+
return (
|
|
217
|
+
a.name === wanted || dir === wanted || dir.endsWith(`/${wanted}`)
|
|
218
|
+
);
|
|
219
|
+
})
|
|
220
|
+
: apps;
|
|
221
|
+
if (candidates.length === 0) {
|
|
222
|
+
return { menu: true, reason: `No app \`${wanted}\`. The apps:` };
|
|
223
|
+
}
|
|
224
|
+
if (candidates.length > 1) {
|
|
225
|
+
return { menu: true, reason: "Which app? Fill in App with one of:" };
|
|
226
|
+
}
|
|
227
|
+
const chosen = candidates[0];
|
|
228
|
+
const found = story
|
|
229
|
+
? chosen.stories.find((s) => s.name === story)
|
|
230
|
+
: chosen.stories.find((s) => s.primary);
|
|
231
|
+
if (!found) {
|
|
232
|
+
return {
|
|
233
|
+
menu: true,
|
|
234
|
+
reason: story
|
|
235
|
+
? `No story \`${story}\` in ${chosen.name}. Pick one:`
|
|
236
|
+
: `${chosen.name} has no primary story. Pick one:`,
|
|
237
|
+
apps: [chosen],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (found.extends) {
|
|
241
|
+
// A variant is made for one brand.
|
|
242
|
+
if (brand && brand !== found.brand) {
|
|
243
|
+
return {
|
|
244
|
+
menu: true,
|
|
245
|
+
reason: `\`${found.name}\` is made for \`${found.brand}\`. Leave Brand empty, or pick \`${found.extends}\`:`,
|
|
246
|
+
apps: [chosen],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { app: chosen.dir, story: found.name, brand: found.brand };
|
|
250
|
+
}
|
|
251
|
+
return withBrand(chosen, found.name, brand || null);
|
|
252
|
+
};
|
package/bin/runner.d.mts
ADDED
package/bin/runner.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Writes the Playwright config and spec that run a story, into the app's
|
|
2
|
+
// demo/out/.runner/. They live in the app (not in this package) for two
|
|
3
|
+
// reasons: Playwright only transforms TypeScript outside node_modules, and
|
|
4
|
+
// they must import the app's own @playwright/test so one Playwright loads.
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
const importPath = (from, to) => {
|
|
10
|
+
const rel = path.relative(from, to).split(path.sep).join("/");
|
|
11
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const HEADER =
|
|
15
|
+
"// Generated by @navipartner/demo-video on every run; do not edit.\n";
|
|
16
|
+
|
|
17
|
+
/** Writes playwright.config.mjs and demo.spec.mjs; returns the config path. */
|
|
18
|
+
export const writeRunner = ({
|
|
19
|
+
runnerDir,
|
|
20
|
+
demoDir,
|
|
21
|
+
adapterPath,
|
|
22
|
+
configPath,
|
|
23
|
+
}) => {
|
|
24
|
+
fs.mkdirSync(runnerDir, { recursive: true });
|
|
25
|
+
const adapter = JSON.stringify(importPath(runnerDir, adapterPath));
|
|
26
|
+
const dirs = `${JSON.stringify(demoDir)}, ${JSON.stringify(runnerDir)}`;
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
configPath,
|
|
29
|
+
`${HEADER}import { createDemoConfig } from "@navipartner/demo-video/runner";
|
|
30
|
+
|
|
31
|
+
import adapter from ${adapter};
|
|
32
|
+
|
|
33
|
+
export default createDemoConfig(adapter, ${dirs});
|
|
34
|
+
`,
|
|
35
|
+
);
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
path.join(runnerDir, "demo.spec.mjs"),
|
|
38
|
+
`${HEADER}import { registerDemoTests } from "@navipartner/demo-video/runner";
|
|
39
|
+
import { test } from "@playwright/test";
|
|
40
|
+
|
|
41
|
+
import adapter from ${adapter};
|
|
42
|
+
|
|
43
|
+
registerDemoTests(test, adapter, ${JSON.stringify(demoDir)});
|
|
44
|
+
`,
|
|
45
|
+
);
|
|
46
|
+
return configPath;
|
|
47
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 441.60001 41.099998" xml:space="preserve" sodipodi:docname="NaviPartner_rgb.svg" width="441.60001" height="41.099998" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"><defs id="defs335"></defs>
|
|
4
|
+
<style type="text/css" id="style308">
|
|
5
|
+
.st0{fill:#729FD3;}
|
|
6
|
+
.st1{fill:#123A7E;}
|
|
7
|
+
</style>
|
|
8
|
+
<path class="st0" d="m 11.42,14.87 21.04,23.39 c 1.63,1.81 3.94,2.84 6.37,2.84 H 48.4 V 0 H 36.99 V 26.22 L 15.93,2.82 C 14.31,1.02 12.01,0 9.59,0 H 0 v 41.1 h 11.42 z" id="path310"></path>
|
|
9
|
+
<path class="st0" d="M 66.51,41.1 82.43,11.92 98.34,41.1 h 13 L 88.92,0 H 80.98 C 77.86,0 74.99,1.7 73.5,4.44 L 53.51,41.1 Z" id="path312"></path>
|
|
10
|
+
<path class="st0" d="M 137.46,36.47 157.35,0 h -13 L 128.44,29.18 112.52,0 h -13 l 22.42,41.1 h 7.72 c 3.25,0 6.25,-1.78 7.8,-4.63 z" id="path314"></path>
|
|
11
|
+
<path class="st0" d="M 172.8,41.1 V 0 h -3.5 C 164.71,0 161,3.72 161,8.3 v 32.8 z" id="path316"></path>
|
|
12
|
+
<path class="st1" d="m 186.11,0 h 19.9 c 3.05,0 5.51,0.86 7.37,2.58 1.86,1.72 2.79,3.99 2.79,6.81 v 6.05 c 0,3.01 -1,5.45 -2.99,7.31 -2,1.86 -4.62,2.79 -7.87,2.79 H 190.46 V 41.1 h -4.34 V 0 Z m 19.08,21.67 c 2,0 3.6,-0.58 4.81,-1.73 1.21,-1.15 1.82,-2.69 1.82,-4.61 V 9.28 c 0,-1.6 -0.54,-2.91 -1.61,-3.9 -1.08,-1 -2.52,-1.5 -4.31,-1.5 h -15.44 v 17.79 z" id="path318"></path>
|
|
13
|
+
<path class="st1" d="M 310,3.76 H 295.21 V 0 h 33.92 V 3.76 H 314.34 V 41.1 H 310 Z" id="path320"></path>
|
|
14
|
+
<path class="st1" d="m 334.72,0 h 4.11 l 22.78,34.23 V 0 h 4.23 v 41.1 h -4.11 L 338.95,7.11 V 41.1 h -4.23 z" id="path322"></path>
|
|
15
|
+
<path class="st1" d="m 376.14,40.07 c -0.67,-0.68 -1,-1.63 -1,-2.85 V 0 h 27.59 V 3.76 H 379.48 V 21.8 h 14.82 v 3.76 h -14.82 v 11.78 h 23.25 v 3.76 h -23.77 c -1.21,0 -2.15,-0.34 -2.82,-1.03 z" id="path324"></path>
|
|
16
|
+
<path class="st1" d="M 441.6,15.44 V 9.39 C 441.6,6.57 440.67,4.3 438.81,2.58 436.95,0.86 434.49,0 431.44,0 h -19.9 v 41.1 h 4.34 V 25.54 h 11.73 l 8.97,15.56 h 5.02 l -9.04,-15.68 c 2.42,-0.29 4.44,-1.18 6.05,-2.67 1.99,-1.86 2.99,-4.29 2.99,-7.31 z m -4.34,-0.12 c 0,1.92 -0.61,3.45 -1.82,4.61 -1.21,1.15 -2.82,1.73 -4.81,1.73 H 415.89 V 3.88 h 15.44 c 1.8,0 3.24,0.5 4.31,1.5 1.08,1 1.61,2.3 1.61,3.9 v 6.04 z" id="path326"></path>
|
|
17
|
+
<path class="st1" d="M 291.63,15.44 V 9.39 C 291.63,6.57 290.7,4.3 288.84,2.58 286.98,0.86 284.52,0 281.47,0 h -19.9 v 41.1 h 4.34 V 25.54 h 11.73 l 8.97,15.56 h 5.02 l -9.04,-15.68 c 2.42,-0.29 4.44,-1.18 6.05,-2.67 1.99,-1.86 2.99,-4.29 2.99,-7.31 z m -4.34,-0.12 c 0,1.92 -0.61,3.45 -1.82,4.61 -1.21,1.15 -2.82,1.73 -4.81,1.73 H 265.92 V 3.88 h 15.44 c 1.8,0 3.24,0.5 4.31,1.5 1.08,1 1.61,2.3 1.61,3.9 v 6.04 z" id="path328"></path>
|
|
18
|
+
<path class="st1" d="m 248.14,41.1 h 4.63 L 244.1,0 h -6.26 -4.42 -2.35 c -2.28,0 -4.29,1.63 -4.76,3.87 l -7.86,37.23 h 4.63 l 3.3,-15.61 h 18.47 z m -20.95,-19.5 3.32,-16 c 0.18,-0.85 0.93,-1.47 1.8,-1.47 h 1.13 4.42 2.59 l 3.57,17.47 z" id="path330"></path>
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tenantName": "NaviPartner",
|
|
3
|
+
"config": {
|
|
4
|
+
"primaryColor": "#1F5494",
|
|
5
|
+
"primaryForegroundColor": "#ffffff",
|
|
6
|
+
"secondaryColor": "#729FD3",
|
|
7
|
+
"surfaceTone": "light",
|
|
8
|
+
"radiusStyle": "soft",
|
|
9
|
+
"fontPair": "editorial"
|
|
10
|
+
},
|
|
11
|
+
"assets": {
|
|
12
|
+
"logo": "navipartner/logo.svg"
|
|
13
|
+
},
|
|
14
|
+
"video": {
|
|
15
|
+
"primary": "#1F5494",
|
|
16
|
+
"ink": "#123A7E",
|
|
17
|
+
"accent": "#729FD3",
|
|
18
|
+
"text": "#232323",
|
|
19
|
+
"headingFont": "Merriweather",
|
|
20
|
+
"bodyFont": "Work Sans"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Frame, Page } from '@playwright/test';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
interface Brand {
|
|
5
|
+
/** Folder name; letters, digits and dashes. */
|
|
6
|
+
name: string;
|
|
7
|
+
tenantName: string;
|
|
8
|
+
/** Opaque to the engine; the adapter decides how to apply it. */
|
|
9
|
+
config: Record<string, unknown>;
|
|
10
|
+
/** Asset slot → file path relative to the brands folder. */
|
|
11
|
+
assets: Record<string, string>;
|
|
12
|
+
/** Asset slot → URL the stage serves it on (for the adapter's mocks). */
|
|
13
|
+
assetUrls: Record<string, string>;
|
|
14
|
+
/** Absolute brands folder the asset paths are relative to. */
|
|
15
|
+
assetsDir: string;
|
|
16
|
+
stage: {
|
|
17
|
+
background: string;
|
|
18
|
+
};
|
|
19
|
+
/** The video's own look: cards, background, fonts (see the skill's look.md). */
|
|
20
|
+
video: VideoBrand;
|
|
21
|
+
/** Replaces white fills in an SVG logo. */
|
|
22
|
+
logoColor?: string;
|
|
23
|
+
}
|
|
24
|
+
interface VideoBrand {
|
|
25
|
+
/** Asset slot shown on cards; default "logo" when the brand has one. */
|
|
26
|
+
logo?: string;
|
|
27
|
+
/** Full-bleed cards, the italic emphasis word, background tint. */
|
|
28
|
+
primary: string;
|
|
29
|
+
/** Headings on pale cards. */
|
|
30
|
+
ink: string;
|
|
31
|
+
/** Soft background bloom. */
|
|
32
|
+
accent: string;
|
|
33
|
+
/** Sub-lines and eyebrows. */
|
|
34
|
+
text: string;
|
|
35
|
+
headingFont: string;
|
|
36
|
+
bodyFont: string;
|
|
37
|
+
/** Google Fonts css2 URL; built from the font names when omitted. */
|
|
38
|
+
fontsUrl: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A testid (letters, digits, dash, underscore) or a Playwright selector. */
|
|
42
|
+
interface Target {
|
|
43
|
+
target: string;
|
|
44
|
+
/** Pick the nth match (0-based; -1 = last). Default: first. */
|
|
45
|
+
nth?: number;
|
|
46
|
+
}
|
|
47
|
+
/** A full-frame text card; see look.md. */
|
|
48
|
+
interface CardSpec {
|
|
49
|
+
eyebrow?: string;
|
|
50
|
+
/** `*word*` is set in italic. */
|
|
51
|
+
heading?: string;
|
|
52
|
+
sub?: string;
|
|
53
|
+
logo?: boolean;
|
|
54
|
+
style: "pale" | "brand";
|
|
55
|
+
seconds: number;
|
|
56
|
+
}
|
|
57
|
+
interface DirectorApi {
|
|
58
|
+
/** The app's iframe, for actions that talk to the app directly. */
|
|
59
|
+
readonly app: Frame;
|
|
60
|
+
/** True in `check`: no camera, cursor, cards or holds. */
|
|
61
|
+
readonly fast: boolean;
|
|
62
|
+
zoomTo(target: Target, scale: number): Promise<void>;
|
|
63
|
+
zoomOut(options?: {
|
|
64
|
+
park?: boolean;
|
|
65
|
+
}): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* One shot that holds every target (a button and what it changes): pushed
|
|
68
|
+
* in as far as they all fit, up to `maxScale`. Targets that only fit in
|
|
69
|
+
* the wide shot fail the step.
|
|
70
|
+
*/
|
|
71
|
+
frame(targets: Target[], options?: {
|
|
72
|
+
maxScale?: number;
|
|
73
|
+
}): Promise<void>;
|
|
74
|
+
/** zoom: push in to that scale first; omitted = leave the camera. */
|
|
75
|
+
click(target: Target, options?: {
|
|
76
|
+
zoom?: number;
|
|
77
|
+
}): Promise<void>;
|
|
78
|
+
type(target: Target, text: string, perCharMs?: number): Promise<void>;
|
|
79
|
+
press(key: string): Promise<void>;
|
|
80
|
+
park(): Promise<void>;
|
|
81
|
+
waitFor(target: Target, options: {
|
|
82
|
+
hidden?: boolean;
|
|
83
|
+
timeoutMs: number;
|
|
84
|
+
}): Promise<void>;
|
|
85
|
+
expectText(target: Target, text: string, options: {
|
|
86
|
+
contains?: boolean;
|
|
87
|
+
timeoutMs: number;
|
|
88
|
+
}): Promise<void>;
|
|
89
|
+
hold(ms: number): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Runs an adapter action. While filming, time is stopped, so an action that
|
|
92
|
+
* waits on the app (a scan that fetches) needs frames filmed meanwhile.
|
|
93
|
+
*/
|
|
94
|
+
during<T>(work: () => Promise<T>): Promise<T>;
|
|
95
|
+
/**
|
|
96
|
+
* The app's clock, in ms: real time in check mode, the filmed (virtual)
|
|
97
|
+
* time while filming. The idle accounting measures waits with it.
|
|
98
|
+
*/
|
|
99
|
+
clockMs(): number;
|
|
100
|
+
/**
|
|
101
|
+
* Dissolves the card in over the app and holds it. It dissolves back out
|
|
102
|
+
* unless `keepUp`: then the next card replaces it directly.
|
|
103
|
+
*/
|
|
104
|
+
card(spec: CardSpec, options?: {
|
|
105
|
+
keepUp?: boolean;
|
|
106
|
+
}): Promise<void>;
|
|
107
|
+
/** Testids currently visible in the app, for failure reports. */
|
|
108
|
+
visibleTestIds(): Promise<string[]>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface DemoAction<S extends z.ZodRawShape = z.ZodRawShape> {
|
|
112
|
+
params: z.ZodObject<S>;
|
|
113
|
+
run(d: DirectorApi, params: z.infer<z.ZodObject<S>>): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
interface DemoAdapter {
|
|
116
|
+
/** Must equal the storyboard's `app`. */
|
|
117
|
+
app: string;
|
|
118
|
+
webServer: {
|
|
119
|
+
command: string;
|
|
120
|
+
url: string;
|
|
121
|
+
env?: Record<string, string>;
|
|
122
|
+
timeoutSeconds?: number;
|
|
123
|
+
};
|
|
124
|
+
/** Path the stage loads in its iframe, e.g. "/?tenant=demo". */
|
|
125
|
+
appPath: string;
|
|
126
|
+
/**
|
|
127
|
+
* The app's screen in CSS pixels. Default 1728×972 (landscape). A portrait
|
|
128
|
+
* kiosk such as 1080×1920 is scaled to fit and centred on the stage.
|
|
129
|
+
*/
|
|
130
|
+
viewport?: {
|
|
131
|
+
width: number;
|
|
132
|
+
height: number;
|
|
133
|
+
};
|
|
134
|
+
locale?: string;
|
|
135
|
+
timezone?: string;
|
|
136
|
+
/** Testid that means "the mocks booted the app". */
|
|
137
|
+
ready: string;
|
|
138
|
+
/** The app's own idle timeout; `check` and `render` keep holds under it. */
|
|
139
|
+
idleTimeoutSeconds?: number;
|
|
140
|
+
/** Installs mocks and applies the brand. Runs before the app loads. */
|
|
141
|
+
setup(page: Page, ctx: {
|
|
142
|
+
brand: Brand;
|
|
143
|
+
}): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* The app's own brands folder, relative to demo/ (default "./brands").
|
|
146
|
+
* Searched before the brands shipped with the engine (navipartner, ...).
|
|
147
|
+
*/
|
|
148
|
+
brands?: string;
|
|
149
|
+
/** Default "navipartner". */
|
|
150
|
+
defaultBrand?: string;
|
|
151
|
+
/** App-specific steps, used in storyboards as { "do": "<name>", …params }. */
|
|
152
|
+
actions?: Record<string, DemoAction<any>>;
|
|
153
|
+
}
|
|
154
|
+
declare const defineDemo: (adapter: DemoAdapter) => DemoAdapter;
|
|
155
|
+
declare const defineAction: <S extends z.ZodRawShape>(action: DemoAction<S>) => DemoAction<S>;
|
|
156
|
+
|
|
157
|
+
export { type Brand as B, type CardSpec as C, type DemoAdapter as D, type Target as T, type VideoBrand as V, type DemoAction as a, type DirectorApi as b, defineDemo as c, defineAction as d };
|