@prosopo/client-bundle-example 2.10.18 → 2.10.20
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/.turbo/turbo-build$colon$cjs.log +9 -7
- package/.turbo/turbo-build$colon$tsc.log +11 -11
- package/.turbo/turbo-build.log +11 -8
- package/CHANGELOG.md +10 -0
- package/dist/_virtual/_rolldown/runtime.js +3 -0
- package/dist/cjs/index.cjs +39 -48
- package/dist/index.js +41 -47
- package/package.json +11 -9
- package/src/plugins/explanation-injector.ts +2 -2
- package/src/plugins/form-filler-injector.ts +2 -2
- package/src/plugins/navigation-injector.ts +2 -2
- package/src/plugins/status-log-injector.ts +2 -2
- package/src/tests/floatLabel.unit.test.ts +195 -0
- package/src/tests/floatLabelReady.unit.test.ts +36 -0
- package/src/tests/plugins.unit.test.ts +256 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +19 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
// The demo playground's vite plugins rewrite each demo page's HTML at serve and
|
|
16
|
+
// build time. They run over hand-written HTML, so the important behaviour is
|
|
17
|
+
// what they do with malformed or already-transformed input: they must return
|
|
18
|
+
// the html untouched rather than producing a broken page.
|
|
19
|
+
|
|
20
|
+
import type { IndexHtmlTransformContext, Plugin } from "vite";
|
|
21
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
22
|
+
import explanationInjector from "../plugins/explanation-injector.js";
|
|
23
|
+
import formFillerInjector from "../plugins/form-filler-injector.js";
|
|
24
|
+
import navigationInjector from "../plugins/navigation-injector.js";
|
|
25
|
+
import statusLogInjector from "../plugins/status-log-injector.js";
|
|
26
|
+
|
|
27
|
+
type Handler = (html: string, ctx: IndexHtmlTransformContext) => string;
|
|
28
|
+
|
|
29
|
+
// transformIndexHtml is declared in its object form ({ order, handler }) by
|
|
30
|
+
// every plugin here; pull the handler out so the tests can call it directly.
|
|
31
|
+
const getHandler = (plugin: Plugin): Handler => {
|
|
32
|
+
const transform = plugin.transformIndexHtml;
|
|
33
|
+
if (
|
|
34
|
+
!transform ||
|
|
35
|
+
"function" === typeof transform ||
|
|
36
|
+
!("handler" in transform) ||
|
|
37
|
+
!transform.handler
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(`${plugin.name} has no object-form transformIndexHtml`);
|
|
40
|
+
}
|
|
41
|
+
return transform.handler as unknown as Handler;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ctx = (filename: string): IndexHtmlTransformContext =>
|
|
45
|
+
({ filename, path: `/${filename}` }) as IndexHtmlTransformContext;
|
|
46
|
+
|
|
47
|
+
const page = (body = "<p>hello</p>"): string =>
|
|
48
|
+
`<html><head><title>demo</title></head><body>${body}</body></html>`;
|
|
49
|
+
|
|
50
|
+
describe("formFillerInjector", () => {
|
|
51
|
+
const handler = getHandler(formFillerInjector());
|
|
52
|
+
|
|
53
|
+
it("is registered as a post transform", () => {
|
|
54
|
+
const transform = formFillerInjector().transformIndexHtml;
|
|
55
|
+
|
|
56
|
+
expect(transform).toMatchObject({ order: "post" });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("injects the button, its style and its script", () => {
|
|
60
|
+
const html = handler(page(), ctx("src/index.html"));
|
|
61
|
+
|
|
62
|
+
expect(html).toContain('id="form-filler-button"');
|
|
63
|
+
expect(html).toContain(".form-filler-button");
|
|
64
|
+
expect(html).toContain("fillFormWithDefaults");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("keeps the injected markup inside the body", () => {
|
|
68
|
+
const html = handler(page(), ctx("src/index.html"));
|
|
69
|
+
|
|
70
|
+
expect(html.indexOf('id="form-filler-button"')).toBeLessThan(
|
|
71
|
+
html.indexOf("</body>"),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("leaves html without a body untouched", () => {
|
|
76
|
+
const fragment = "<div>no body here</div>";
|
|
77
|
+
|
|
78
|
+
expect(handler(fragment, ctx("src/index.html"))).toBe(fragment);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("leaves html with an opening but no closing body untouched", () => {
|
|
82
|
+
const fragment = "<html><body><div>truncated</div>";
|
|
83
|
+
|
|
84
|
+
expect(handler(fragment, ctx("src/index.html"))).toBe(fragment);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("statusLogInjector", () => {
|
|
89
|
+
const handler = getHandler(statusLogInjector());
|
|
90
|
+
|
|
91
|
+
it("injects the status log container, css and js", () => {
|
|
92
|
+
const html = handler(page(), ctx("src/index.html"));
|
|
93
|
+
|
|
94
|
+
expect(html).toContain('id="captcha-status"');
|
|
95
|
+
expect(html).toContain("updateCaptchaStatus");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("places the status log directly after a form when one exists", () => {
|
|
99
|
+
const html = handler(
|
|
100
|
+
page("<form><input name='a'/></form>"),
|
|
101
|
+
ctx("src/index.html"),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(html.indexOf('id="captcha-status"')).toBeGreaterThan(
|
|
105
|
+
html.indexOf("</form>"),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("falls back to the end of the body when there is no form", () => {
|
|
110
|
+
const html = handler(page(), ctx("src/index.html"));
|
|
111
|
+
|
|
112
|
+
expect(html.indexOf('id="captcha-status"')).toBeLessThan(
|
|
113
|
+
html.indexOf("</body>"),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("is idempotent — a page that already has a status log is untouched", () => {
|
|
118
|
+
const once = handler(page(), ctx("src/index.html"));
|
|
119
|
+
|
|
120
|
+
expect(handler(once, ctx("src/index.html"))).toBe(once);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
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);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
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"));
|
|
151
|
+
|
|
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"));
|
|
157
|
+
|
|
158
|
+
expect(html).toContain("Implicit");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns the html unchanged for a page it cannot classify", () => {
|
|
162
|
+
const html = page();
|
|
163
|
+
|
|
164
|
+
expect(handler(html, ctx("src/some-other-page.html"))).toBe(html);
|
|
165
|
+
});
|
|
166
|
+
|
|
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
|
+
});
|
|
172
|
+
|
|
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>',
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const html = handler(withExplanation, ctx("src/image-explicit.html"));
|
|
179
|
+
|
|
180
|
+
expect(html).not.toContain("stale copy");
|
|
181
|
+
expect(html.match(/<div class="explanation">/g)).toHaveLength(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
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);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
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
|
+
});
|
|
202
|
+
|
|
203
|
+
it("is registered as a pre transform", () => {
|
|
204
|
+
expect(navigationInjector().transformIndexHtml).toMatchObject({
|
|
205
|
+
order: "pre",
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("injects the nav bar with both standard and invisible sections", () => {
|
|
210
|
+
const html = handler(page(), ctx("src/index.html"));
|
|
211
|
+
|
|
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">');
|
|
222
|
+
});
|
|
223
|
+
|
|
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`));
|
|
227
|
+
|
|
228
|
+
expect(html).not.toContain('class="active"');
|
|
229
|
+
});
|
|
230
|
+
|
|
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`));
|
|
234
|
+
|
|
235
|
+
expect(html).toContain('href="../index.html"');
|
|
236
|
+
});
|
|
237
|
+
|
|
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">');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("returns the html unchanged when there is no body tag", () => {
|
|
246
|
+
const fragment = "<div>no body here</div>";
|
|
247
|
+
|
|
248
|
+
expect(handler(fragment, ctx("src/index.html"))).toBe(fragment);
|
|
249
|
+
});
|
|
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
|
+
});
|