@prosopo/client-bundle-example 2.10.24 → 2.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.
Files changed (51) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +5 -4
  2. package/.turbo/turbo-build$colon$tsc.log +11 -11
  3. package/.turbo/turbo-build.log +6 -5
  4. package/CHANGELOG.md +16 -0
  5. package/dist/cjs/index.cjs +13 -41
  6. package/dist/cjs/plugins/pages.cjs +113 -0
  7. package/dist/index.js +2 -42
  8. package/dist/plugins/pages.js +103 -0
  9. package/package.json +2 -2
  10. package/src/frictionless-explicit-web3.html +10 -23
  11. package/src/frictionless-explicit.html +10 -22
  12. package/src/frictionless-implicit.html +60 -86
  13. package/src/frictionless-manual-start.html +13 -33
  14. package/src/image-explicit-web3.html +8 -24
  15. package/src/image-explicit.html +8 -23
  16. package/src/image-implicit.html +192 -0
  17. package/src/index.html +11 -26
  18. package/src/index.ts +1 -62
  19. package/src/invisible-frictionless-explicit.html +8 -17
  20. package/src/invisible-frictionless-implicit.html +8 -25
  21. package/src/invisible-image-explicit.html +8 -23
  22. package/src/invisible-image-implicit.html +8 -24
  23. package/src/invisible-pow-explicit.html +8 -23
  24. package/src/invisible-pow-implicit.html +9 -21
  25. package/src/invisible-puzzle-explicit.html +8 -23
  26. package/src/invisible-puzzle-implicit.html +9 -21
  27. package/src/plugins/code-snippet-injector.ts +179 -0
  28. package/src/plugins/form-filler-injector.ts +36 -121
  29. package/src/plugins/layout-injector.ts +238 -0
  30. package/src/plugins/logo.ts +15 -0
  31. package/src/plugins/pages.ts +219 -0
  32. package/src/plugins/placement-injector.ts +25 -64
  33. package/src/plugins/slots.ts +32 -0
  34. package/src/plugins/status-log-injector.ts +35 -114
  35. package/src/pow-explicit-web3.html +8 -24
  36. package/src/pow-explicit.html +8 -23
  37. package/src/pow-implicit-sessionid.html +10 -24
  38. package/src/pow-implicit.html +9 -24
  39. package/src/puzzle-bind-explicit.html +9 -24
  40. package/src/puzzle-explicit.html +8 -23
  41. package/src/puzzle-implicit.html +9 -24
  42. package/src/styles/demo.css +803 -0
  43. package/src/tests/plugins.unit.test.ts +285 -118
  44. package/tsconfig.tsbuildinfo +1 -1
  45. package/vite.config.ts +5 -5
  46. package/src/plugins/explanation-injector.ts +0 -294
  47. package/src/plugins/navigation-injector.ts +0 -606
  48. package/src/styles/captcha.css +0 -78
  49. package/src/styles/field.css +0 -12
  50. package/src/tests/floatLabel.unit.test.ts +0 -195
  51. package/src/tests/floatLabelReady.unit.test.ts +0 -36
@@ -17,11 +17,25 @@
17
17
  // what they do with malformed or already-transformed input: they must return
18
18
  // the html untouched rather than producing a broken page.
19
19
 
20
+ import { CaptchaType } from "@prosopo/types";
20
21
  import type { IndexHtmlTransformContext, Plugin } from "vite";
21
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
- import explanationInjector from "../plugins/explanation-injector.js";
22
+ import { describe, expect, it } from "vitest";
23
+ import codeSnippetInjector, {
24
+ snippetFor,
25
+ } from "../plugins/code-snippet-injector.js";
23
26
  import formFillerInjector from "../plugins/form-filler-injector.js";
24
- import navigationInjector from "../plugins/navigation-injector.js";
27
+ import layoutInjector from "../plugins/layout-injector.js";
28
+ import {
29
+ type DemoRendering,
30
+ demoPages,
31
+ findPageByPath,
32
+ pagePathFromFilename,
33
+ relativePrefix,
34
+ renderingsFor,
35
+ resolvePage,
36
+ } from "../plugins/pages.js";
37
+ import placementInjector from "../plugins/placement-injector.js";
38
+ import { fillSlot, slotMarkup } from "../plugins/slots.js";
25
39
  import statusLogInjector from "../plugins/status-log-injector.js";
26
40
 
27
41
  type Handler = (html: string, ctx: IndexHtmlTransformContext) => string;
@@ -47,63 +61,259 @@ const ctx = (filename: string): IndexHtmlTransformContext =>
47
61
  const page = (body = "<p>hello</p>"): string =>
48
62
  `<html><head><title>demo</title></head><body>${body}</body></html>`;
49
63
 
50
- describe("formFillerInjector", () => {
51
- const handler = getHandler(formFillerInjector());
64
+ const noBody = "<div>no body here</div>";
52
65
 
53
- it("is registered as a post transform", () => {
54
- const transform = formFillerInjector().transformIndexHtml;
66
+ const cwd = process.cwd();
67
+
68
+ describe("pages", () => {
69
+ it("has one page per setup and one setup per page", () => {
70
+ const setups = demoPages.map(
71
+ ({ captchaType, mode, rendering }) =>
72
+ `${captchaType}/${mode}/${rendering}`,
73
+ );
74
+ const paths = demoPages.map(({ path }) => path);
55
75
 
56
- expect(transform).toMatchObject({ order: "post" });
76
+ expect(new Set(setups).size).toBe(demoPages.length);
77
+ expect(new Set(paths).size).toBe(demoPages.length);
57
78
  });
58
79
 
59
- it("injects the button, its style and its script", () => {
60
- const html = handler(page(), ctx("src/index.html"));
80
+ it("makes standard implicit frictionless the index page", () => {
81
+ expect(findPageByPath("index.html")).toMatchObject({
82
+ captchaType: CaptchaType.frictionless,
83
+ mode: "standard",
84
+ rendering: "implicit",
85
+ });
86
+ });
61
87
 
62
- expect(html).toContain('id="form-filler-button"');
63
- expect(html).toContain(".form-filler-button");
64
- expect(html).toContain("fillFormWithDefaults");
88
+ it("falls back to implicit rendering when a setup has no page", () => {
89
+ const resolved = resolvePage({
90
+ captchaType: CaptchaType.frictionless,
91
+ mode: "invisible",
92
+ rendering: "manual",
93
+ });
94
+
95
+ expect(resolved.path).toBe("invisible-frictionless-implicit.html");
65
96
  });
66
97
 
67
- it("keeps the injected markup inside the body", () => {
68
- const html = handler(page(), ctx("src/index.html"));
98
+ it("only offers renderings that have a page", () => {
99
+ const values = (captchaType: CaptchaType.pow | CaptchaType.puzzle) =>
100
+ renderingsFor(captchaType, "standard").map(
101
+ ({ value }): DemoRendering => value,
102
+ );
103
+
104
+ expect(values(CaptchaType.pow)).toEqual(["implicit", "explicit"]);
105
+ expect(values(CaptchaType.puzzle)).toEqual([
106
+ "implicit",
107
+ "explicit",
108
+ "bound",
109
+ ]);
110
+ });
69
111
 
70
- expect(html.indexOf('id="form-filler-button"')).toBeLessThan(
112
+ it("reads the page path from a src, dist or absolute filename", () => {
113
+ expect(pagePathFromFilename("src/index.html")).toBe("index.html");
114
+ expect(pagePathFromFilename(`${cwd}/dist/pow-explicit.html`)).toBe(
115
+ "pow-explicit.html",
116
+ );
117
+ expect(pagePathFromFilename(`${cwd}/src/nested/deep.html`)).toBe(
118
+ "nested/deep.html",
119
+ );
120
+ expect(pagePathFromFilename(undefined)).toBe("");
121
+ });
122
+
123
+ it("climbs out of nested pages", () => {
124
+ expect(relativePrefix("index.html")).toBe("");
125
+ expect(relativePrefix("nested/deep.html")).toBe("../");
126
+ });
127
+ });
128
+
129
+ describe("slots", () => {
130
+ it("returns undefined when the slot is not on the page", () => {
131
+ expect(fillSlot(page(), "events", "x")).toBeUndefined();
132
+ });
133
+
134
+ it("inserts content literally, even when it looks like a replace pattern", () => {
135
+ const html = fillSlot(page(slotMarkup("events")), "events", "$&");
136
+
137
+ expect(html).toBe(page("$&"));
138
+ });
139
+ });
140
+
141
+ describe("layoutInjector", () => {
142
+ const handler = getHandler(layoutInjector());
143
+
144
+ it("is registered as a pre transform", () => {
145
+ expect(layoutInjector().transformIndexHtml).toMatchObject({
146
+ order: "pre",
147
+ });
148
+ });
149
+
150
+ it("wraps the page in the header, sidebar and panel with a slot for each plugin", () => {
151
+ const html = handler(page(), ctx(`${cwd}/src/pow-explicit.html`));
152
+
153
+ expect(html).toContain('<body class="demo">');
154
+ expect(html).toContain('class="demo-header"');
155
+ expect(html).toContain('href="styles/demo.css"');
156
+ for (const slot of ["placement", "toolbar", "events", "code"] as const) {
157
+ expect(html).toContain(slotMarkup(slot));
158
+ }
159
+ expect(html.indexOf("<p>hello</p>")).toBeLessThan(
160
+ html.indexOf('class="demo-panel"'),
161
+ );
162
+ });
163
+
164
+ it("marks the current type, mode and rendering", () => {
165
+ const html = handler(page(), ctx(`${cwd}/src/pow-explicit.html`));
166
+
167
+ expect(html.match(/aria-current="page"/g)).toHaveLength(3);
168
+ expect(html).toContain(
169
+ '<a class="demo-choice" data-demo-nav href="pow-explicit.html" aria-current="page">',
170
+ );
171
+ });
172
+
173
+ it("folds rendering away unless the page uses a non-default rendering", () => {
174
+ const implicit = handler(page(), ctx(`${cwd}/src/pow-implicit.html`));
175
+ const explicit = handler(page(), ctx(`${cwd}/src/pow-explicit.html`));
176
+
177
+ expect(implicit).toContain('<details class="demo-advanced">');
178
+ expect(explicit).toContain('<details class="demo-advanced" open>');
179
+ });
180
+
181
+ it("keeps the mode and rendering when switching captcha type", () => {
182
+ const html = handler(
183
+ page(),
184
+ ctx(`${cwd}/src/invisible-image-explicit.html`),
185
+ );
186
+
187
+ expect(html).toContain('href="invisible-pow-explicit.html"');
188
+ expect(html).toContain('href="invisible-puzzle-explicit.html"');
189
+ });
190
+
191
+ it("marks nothing current and shows no code for a page outside the registry", () => {
192
+ const html = handler(page(), ctx(`${cwd}/src/pow-implicit-sessionid.html`));
193
+
194
+ expect(html).not.toContain('aria-current="page"');
195
+ expect(html).not.toContain(slotMarkup("code"));
196
+ expect(html).toContain('href="index.html"');
197
+ });
198
+
199
+ it("resolves links relative to a nested page", () => {
200
+ const html = handler(page(), ctx(`${cwd}/src/nested/deep.html`));
201
+
202
+ expect(html).toContain('href="../index.html"');
203
+ expect(html).toContain('href="../styles/demo.css"');
204
+ });
205
+
206
+ it("tolerates a context with no filename", () => {
207
+ const html = handler(page(), {} as IndexHtmlTransformContext);
208
+
209
+ expect(html).toContain('class="demo-header"');
210
+ });
211
+
212
+ it("returns the html unchanged when there is no body tag", () => {
213
+ expect(handler(noBody, ctx("src/index.html"))).toBe(noBody);
214
+ });
215
+ });
216
+
217
+ describe("codeSnippetInjector", () => {
218
+ const handler = getHandler(codeSnippetInjector());
219
+
220
+ it("fills the code slot with the escaped snippet for the page", () => {
221
+ const html = handler(
222
+ page(slotMarkup("code")),
223
+ ctx(`${cwd}/src/frictionless-explicit.html`),
224
+ );
225
+
226
+ expect(html).not.toContain(slotMarkup("code"));
227
+ expect(html).toContain("data-demo-copy");
228
+ expect(html).toContain("&lt;div id=&quot;procaptcha-container&quot;&gt;");
229
+ });
230
+
231
+ it("falls back to the end of the body when there is no slot", () => {
232
+ const html = handler(page(), ctx(`${cwd}/src/index.html`));
233
+
234
+ expect(html.indexOf("data-demo-copy")).toBeLessThan(
71
235
  html.indexOf("</body>"),
72
236
  );
73
237
  });
74
238
 
75
- it("leaves html without a body untouched", () => {
76
- const fragment = "<div>no body here</div>";
239
+ it("gives every demo page a snippet that uses a placeholder site key", () => {
240
+ for (const demoPage of demoPages) {
241
+ expect(snippetFor(demoPage)).toContain("YOUR_SITE_KEY");
242
+ }
243
+ });
77
244
 
78
- expect(handler(fragment, ctx("src/index.html"))).toBe(fragment);
245
+ it("matches the snippet to the rendering and mode", () => {
246
+ const snippet = (path: string): string => {
247
+ const demoPage = findPageByPath(path);
248
+ if (!demoPage) {
249
+ throw new Error(`${path} is not a demo page`);
250
+ }
251
+ return snippetFor(demoPage);
252
+ };
253
+
254
+ expect(snippet("pow-implicit.html")).toContain('class="procaptcha"');
255
+ expect(snippet("frictionless-manual-start.html")).toContain(
256
+ 'data-start-mode="manual"',
257
+ );
258
+ expect(snippet("puzzle-bind-explicit.html")).toContain("bind:");
259
+ expect(snippet("invisible-image-implicit.html")).toContain(
260
+ 'data-size="invisible"',
261
+ );
262
+ expect(snippet("invisible-image-explicit.html")).toContain("execute()");
79
263
  });
80
264
 
81
- it("leaves html with an opening but no closing body untouched", () => {
82
- const fragment = "<html><body><div>truncated</div>";
265
+ it("returns the html unchanged for a page outside the registry", () => {
266
+ const html = page();
83
267
 
84
- expect(handler(fragment, ctx("src/index.html"))).toBe(fragment);
268
+ expect(handler(html, ctx(`${cwd}/src/some-other-page.html`))).toBe(html);
269
+ expect(handler(html, {} as IndexHtmlTransformContext)).toBe(html);
270
+ });
271
+
272
+ it("is idempotent", () => {
273
+ const once = handler(page(), ctx(`${cwd}/src/index.html`));
274
+
275
+ expect(handler(once, ctx(`${cwd}/src/index.html`))).toBe(once);
276
+ });
277
+
278
+ it("leaves html without a body untouched", () => {
279
+ expect(handler(noBody, ctx(`${cwd}/src/index.html`))).toBe(noBody);
85
280
  });
86
281
  });
87
282
 
88
283
  describe("statusLogInjector", () => {
89
284
  const handler = getHandler(statusLogInjector());
90
285
 
91
- it("injects the status log container, css and js", () => {
286
+ it("injects the status log container and js", () => {
92
287
  const html = handler(page(), ctx("src/index.html"));
93
288
 
94
289
  expect(html).toContain('id="captcha-status"');
95
290
  expect(html).toContain("updateCaptchaStatus");
96
291
  });
97
292
 
98
- it("places the status log directly after a form when one exists", () => {
293
+ it("fills the events slot when the layout provides one", () => {
294
+ const html = handler(
295
+ page(`<form></form>${slotMarkup("events")}`),
296
+ ctx("src/index.html"),
297
+ );
298
+
299
+ expect(html).not.toContain(slotMarkup("events"));
300
+ expect(html.indexOf('id="captcha-status"')).toBeGreaterThan(
301
+ html.indexOf("</form>"),
302
+ );
303
+ });
304
+
305
+ it("places the status log directly after a form when there is no slot", () => {
99
306
  const html = handler(
100
- page("<form><input name='a'/></form>"),
307
+ page("<form><input name='a'/></form><p>after</p>"),
101
308
  ctx("src/index.html"),
102
309
  );
103
310
 
104
311
  expect(html.indexOf('id="captcha-status"')).toBeGreaterThan(
105
312
  html.indexOf("</form>"),
106
313
  );
314
+ expect(html.indexOf('id="captcha-status"')).toBeLessThan(
315
+ html.indexOf("<p>after</p>"),
316
+ );
107
317
  });
108
318
 
109
319
  it("falls back to the end of the body when there is no form", () => {
@@ -121,136 +331,93 @@ describe("statusLogInjector", () => {
121
331
  });
122
332
 
123
333
  it("leaves html without a body untouched", () => {
124
- const fragment = "<div>no body here</div>";
125
-
126
- expect(handler(fragment, ctx("src/index.html"))).toBe(fragment);
334
+ expect(handler(noBody, ctx("src/index.html"))).toBe(noBody);
127
335
  });
128
336
  });
129
337
 
130
- describe("explanationInjector", () => {
131
- const handler = getHandler(explanationInjector());
132
-
133
- const cases: Array<{ file: string; heading: string }> = [
134
- { file: "src/frictionless-explicit.html", heading: "Frictionless CAPTCHA" },
135
- { file: "src/image-explicit.html", heading: "Image CAPTCHA" },
136
- { file: "src/pow-explicit.html", heading: "Proof of Work" },
137
- { file: "src/puzzle-explicit.html", heading: "Puzzle CAPTCHA" },
138
- ];
139
-
140
- for (const { file, heading } of cases) {
141
- it(`generates an explanation for ${file}`, () => {
142
- const html = handler(page(), ctx(file));
143
-
144
- expect(html).toContain('<div class="explanation">');
145
- expect(html).toContain(heading);
146
- });
147
- }
148
-
149
- it("labels an invisible page as invisible", () => {
150
- const html = handler(page(), ctx("src/invisible-image-explicit.html"));
338
+ describe("placementInjector", () => {
339
+ const handler = getHandler(placementInjector());
151
340
 
152
- expect(html).toContain("Invisible");
153
- });
154
-
155
- it("describes implicit rendering for an implicit page", () => {
156
- const html = handler(page(), ctx("src/image-implicit.html"));
341
+ it("fills the placement slot with both options and the script", () => {
342
+ const html = handler(page(slotMarkup("placement")), ctx("src/index.html"));
157
343
 
158
- expect(html).toContain("Implicit");
344
+ expect(html).not.toContain(slotMarkup("placement"));
345
+ expect(html).toContain('data-placement-option="popup"');
346
+ expect(html).toContain('data-placement-option="float"');
347
+ expect(html).toContain("data-placement");
159
348
  });
160
349
 
161
- it("returns the html unchanged for a page it cannot classify", () => {
162
- const html = page();
350
+ it("uses links rather than buttons for the options", () => {
351
+ const html = handler(page(slotMarkup("placement")), ctx("src/index.html"));
163
352
 
164
- expect(handler(html, ctx("src/some-other-page.html"))).toBe(html);
353
+ expect(html).not.toMatch(/<button[^>]*data-placement-option/);
165
354
  });
166
355
 
167
- it("returns the html unchanged when there is no filename at all", () => {
168
- const html = page();
169
-
170
- expect(handler(html, {} as IndexHtmlTransformContext)).toBe(html);
171
- });
356
+ it("goes above the status log when there is no slot", () => {
357
+ const html = handler(
358
+ page('<div id="captcha-status" class="captcha-status"></div>'),
359
+ ctx("src/index.html"),
360
+ );
172
361
 
173
- it("replaces an existing explanation rather than appending a second one", () => {
174
- const withExplanation = page(
175
- '<div class="explanation"><div>stale copy</div></div>',
362
+ expect(html.indexOf('id="placement-switcher"')).toBeLessThan(
363
+ html.indexOf('id="captcha-status"'),
176
364
  );
365
+ });
177
366
 
178
- const html = handler(withExplanation, ctx("src/image-explicit.html"));
367
+ it("is idempotent", () => {
368
+ const once = handler(page(), ctx("src/index.html"));
179
369
 
180
- expect(html).not.toContain("stale copy");
181
- expect(html.match(/<div class="explanation">/g)).toHaveLength(1);
370
+ expect(handler(once, ctx("src/index.html"))).toBe(once);
182
371
  });
183
372
 
184
373
  it("leaves html without a body untouched", () => {
185
- const fragment = "<div>no body here</div>";
186
-
187
- expect(handler(fragment, ctx("src/image-explicit.html"))).toBe(fragment);
374
+ expect(handler(noBody, ctx("src/index.html"))).toBe(noBody);
188
375
  });
189
376
  });
190
377
 
191
- describe("navigationInjector", () => {
192
- const handler = getHandler(navigationInjector());
193
-
194
- beforeEach(() => {
195
- // the plugin logs the file it is processing on every transform
196
- vi.spyOn(console, "log").mockImplementation(() => undefined);
197
- });
198
-
199
- afterEach(() => {
200
- vi.restoreAllMocks();
201
- });
378
+ describe("formFillerInjector", () => {
379
+ const handler = getHandler(formFillerInjector());
202
380
 
203
- it("is registered as a pre transform", () => {
204
- expect(navigationInjector().transformIndexHtml).toMatchObject({
205
- order: "pre",
381
+ it("is registered as a post transform", () => {
382
+ expect(formFillerInjector().transformIndexHtml).toMatchObject({
383
+ order: "post",
206
384
  });
207
385
  });
208
386
 
209
- it("injects the nav bar with both standard and invisible sections", () => {
387
+ it("injects the button and its script", () => {
210
388
  const html = handler(page(), ctx("src/index.html"));
211
389
 
212
- expect(html).toContain('id="nav-topbar"');
213
- expect(html).toContain("Standard Captchas");
214
- expect(html).toContain("Invisible Captchas");
215
- });
216
-
217
- it("marks the current page as active", () => {
218
- const cwd = process.cwd();
219
- const html = handler(page(), ctx(`${cwd}/src/image-explicit.html`));
220
-
221
- expect(html).toContain('<a href="image-explicit.html" class="active">');
390
+ expect(html).toContain('id="form-filler-button"');
391
+ expect(html).toContain("fillFormWithDefaults");
222
392
  });
223
393
 
224
- it("does not mark any link active for a page outside the nav", () => {
225
- const cwd = process.cwd();
226
- const html = handler(page(), ctx(`${cwd}/src/unknown.html`));
394
+ it("puts the button in the toolbar slot when the layout provides one", () => {
395
+ const html = handler(
396
+ page(`<div class="demo-toolbar">${slotMarkup("toolbar")}</div>`),
397
+ ctx("src/index.html"),
398
+ );
227
399
 
228
- expect(html).not.toContain('class="active"');
400
+ expect(html).not.toContain(slotMarkup("toolbar"));
401
+ expect(html).toMatch(
402
+ /<div class="demo-toolbar">\s*<button id="form-filler-button"/,
403
+ );
229
404
  });
230
405
 
231
- it("resolves links relative to a nested page", () => {
232
- const cwd = process.cwd();
233
- const html = handler(page(), ctx(`${cwd}/src/nested/deep.html`));
406
+ it("keeps the injected markup inside the body", () => {
407
+ const html = handler(page(), ctx("src/index.html"));
234
408
 
235
- expect(html).toContain('href="../index.html"');
409
+ expect(html.indexOf('id="form-filler-button"')).toBeLessThan(
410
+ html.indexOf("</body>"),
411
+ );
236
412
  });
237
413
 
238
- it("handles a dist build path as well as a src path", () => {
239
- const cwd = process.cwd();
240
- const html = handler(page(), ctx(`${cwd}/dist/pow-explicit.html`));
241
-
242
- expect(html).toContain('<a href="pow-explicit.html" class="active">');
414
+ it("leaves html without a body untouched", () => {
415
+ expect(handler(noBody, ctx("src/index.html"))).toBe(noBody);
243
416
  });
244
417
 
245
- it("returns the html unchanged when there is no body tag", () => {
246
- const fragment = "<div>no body here</div>";
418
+ it("leaves html with an opening but no closing body untouched", () => {
419
+ const fragment = "<html><body><div>truncated</div>";
247
420
 
248
421
  expect(handler(fragment, ctx("src/index.html"))).toBe(fragment);
249
422
  });
250
-
251
- it("tolerates a context with no filename", () => {
252
- const html = handler(page(), {} as IndexHtmlTransformContext);
253
-
254
- expect(html).toContain('id="nav-topbar"');
255
- });
256
423
  });