@ckeditor/ckeditor5-dev-manual-server 56.2.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/LICENSE.md +16 -0
- package/README.md +17 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +584 -0
- package/dist/manual-test-plugin/collect-pages.d.ts +6 -0
- package/dist/manual-test-plugin/parse-markdown.d.ts +6 -0
- package/dist/manual-test-plugin/plugin.d.ts +7 -0
- package/dist/manual-test-plugin/shell-html.d.ts +13 -0
- package/dist/manual-test-plugin/static-assets.d.ts +10 -0
- package/dist/manual-test-plugin/types.d.ts +18 -0
- package/dist/raw-plugin/plugin.d.ts +6 -0
- package/dist/refresh-plugin/plugin.d.ts +7 -0
- package/dist/utils.d.ts +20 -0
- package/package.json +38 -0
- package/theme/catalog.css +252 -0
- package/theme/catalog.html +57 -0
- package/theme/catalog.ts +162 -0
- package/theme/shell.css +361 -0
- package/theme/shell.html +46 -0
- package/theme/shell.ts +179 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
export interface ManualData {
|
|
6
|
+
displayName: string;
|
|
7
|
+
instructionsHtml: string;
|
|
8
|
+
packageName: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ManualPageEntry {
|
|
11
|
+
displayName: string;
|
|
12
|
+
htmlFilePath: string;
|
|
13
|
+
instructionsFilePath?: string;
|
|
14
|
+
packageName: string;
|
|
15
|
+
scriptFilePath: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
}
|
|
18
|
+
export type ManualTestAssetExtension = "html" | "js" | "md" | "ts";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
import type { Plugin } from "vite";
|
|
6
|
+
export declare const MANUAL_REFRESH_EVENT_NAME = "ckeditor5-manual:refresh-available";
|
|
7
|
+
export declare function refreshPlugin(): Plugin;
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
export interface CachedValue<T> {
|
|
6
|
+
get(): T;
|
|
7
|
+
invalidate(): void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Caches the computed value until the cache is invalidated.
|
|
11
|
+
*/
|
|
12
|
+
export declare function cacheValue<T>(compute: () => T): CachedValue<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Stringifies the values of the given object.
|
|
15
|
+
*/
|
|
16
|
+
export declare function stringifyValues(obj: Record<string, unknown>): Record<string, string>;
|
|
17
|
+
export declare function toPublicFilePath(filePath: string, workspaceRoot: string): string;
|
|
18
|
+
export declare function toPublicSpecifier(relativeFilePath: string): string;
|
|
19
|
+
export declare function toPosixPath(value: string): string;
|
|
20
|
+
export declare function stripLeadingSlash(value: string): string;
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ckeditor/ckeditor5-dev-manual-server",
|
|
3
|
+
"version": "56.2.0",
|
|
4
|
+
"description": "Used to extend Vite to create a manual test server for CKEditor 5.",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"author": "CKSource (http://cksource.com/)",
|
|
7
|
+
"license": "GPL-2.0-or-later",
|
|
8
|
+
"homepage": "https://github.com/ckeditor/ckeditor5-dev/tree/master/packages/ckeditor5-dev-manual-server",
|
|
9
|
+
"bugs": "https://github.com/ckeditor/ckeditor5/issues",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/ckeditor/ckeditor5-dev.git",
|
|
13
|
+
"directory": "packages/ckeditor5-dev-manual-server"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=24.11.0",
|
|
17
|
+
"npm": ">=5.7.1"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"theme"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@ckeditor/ckeditor5-inspector": "^5.0.1",
|
|
28
|
+
"@parse5/tools": "^0.7.0",
|
|
29
|
+
"hast-util-to-html": "^9.0.5",
|
|
30
|
+
"parse5": "^8.0.1",
|
|
31
|
+
"remark-gfm": "^4.0.1",
|
|
32
|
+
"remark-parse": "^11.0.0",
|
|
33
|
+
"remark-rehype": "^11.1.2",
|
|
34
|
+
"unified": "^11.0.5",
|
|
35
|
+
"vite": "^8.0.13",
|
|
36
|
+
"vite-svg-loader": "^5.1.1"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
4
|
+
line-height: 1.5;
|
|
5
|
+
-webkit-font-smoothing: antialiased;
|
|
6
|
+
-moz-osx-font-smoothing: grayscale;
|
|
7
|
+
|
|
8
|
+
--bg: #fafafa;
|
|
9
|
+
--surface: #fff;
|
|
10
|
+
--surface-hover: #f5f5f5;
|
|
11
|
+
--border: #e5e5e5;
|
|
12
|
+
--border-strong: #d4d4d4;
|
|
13
|
+
--text: #0a0a0a;
|
|
14
|
+
--text-muted: #737373;
|
|
15
|
+
--text-subtle: #a3a3a3;
|
|
16
|
+
--accent: #171717;
|
|
17
|
+
--accent-ring: rgb(23 23 23 / 10%);
|
|
18
|
+
--radius-lg: 12px;
|
|
19
|
+
--radius-md: 8px;
|
|
20
|
+
--radius-sm: 6px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* {
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
margin: 0;
|
|
29
|
+
min-height: 100vh;
|
|
30
|
+
background: var(--bg);
|
|
31
|
+
color: var(--text);
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
a {
|
|
36
|
+
color: inherit;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.app {
|
|
40
|
+
max-width: 1280px;
|
|
41
|
+
margin: 0 auto;
|
|
42
|
+
padding: 0 24px 80px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.app__header {
|
|
46
|
+
position: sticky;
|
|
47
|
+
top: 0;
|
|
48
|
+
z-index: 10;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 20px;
|
|
52
|
+
padding: 32px 0 24px;
|
|
53
|
+
background: linear-gradient(var(--bg) 80%, transparent);
|
|
54
|
+
backdrop-filter: blur(8px);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.app__titles {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: baseline;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
gap: 16px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.app__title {
|
|
65
|
+
margin: 0;
|
|
66
|
+
font-size: clamp(24px, 3vw, 32px);
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
letter-spacing: -0.02em;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.app__subtitle {
|
|
72
|
+
margin: 0;
|
|
73
|
+
font-size: 13px;
|
|
74
|
+
color: var(--text-muted);
|
|
75
|
+
font-variant-numeric: tabular-nums;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.app__search {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 10px;
|
|
82
|
+
height: 44px;
|
|
83
|
+
padding: 0 14px;
|
|
84
|
+
background: var(--surface);
|
|
85
|
+
border: 1px solid var(--border);
|
|
86
|
+
border-radius: var(--radius-md);
|
|
87
|
+
transition: border-color 0.12s, box-shadow 0.12s;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.app__search:focus-within {
|
|
91
|
+
border-color: var(--border-strong);
|
|
92
|
+
box-shadow: 0 0 0 4px var(--accent-ring);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.app__search-icon {
|
|
96
|
+
flex-shrink: 0;
|
|
97
|
+
color: var(--text-subtle);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.app__search-input {
|
|
101
|
+
flex: 1;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
font: inherit;
|
|
104
|
+
font-size: 14px;
|
|
105
|
+
color: var(--text);
|
|
106
|
+
background: transparent;
|
|
107
|
+
border: 0;
|
|
108
|
+
outline: none;
|
|
109
|
+
padding: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.app__search-input::-webkit-search-cancel-button {
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.app__search-input::placeholder {
|
|
117
|
+
color: var(--text-muted);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.app__list {
|
|
121
|
+
display: grid;
|
|
122
|
+
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
123
|
+
gap: 16px;
|
|
124
|
+
align-items: start;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.app__empty {
|
|
128
|
+
grid-column: 1 / -1;
|
|
129
|
+
padding: 64px 24px;
|
|
130
|
+
text-align: center;
|
|
131
|
+
background: var(--surface);
|
|
132
|
+
border: 1px dashed var(--border);
|
|
133
|
+
border-radius: var(--radius-lg);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.app__empty-title {
|
|
137
|
+
margin: 0 0 4px;
|
|
138
|
+
font-size: 15px;
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.app__empty-hint {
|
|
143
|
+
margin: 0;
|
|
144
|
+
font-size: 13px;
|
|
145
|
+
color: var(--text-muted);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.pkg {
|
|
149
|
+
background: var(--surface);
|
|
150
|
+
border: 1px solid var(--border);
|
|
151
|
+
border-radius: var(--radius-lg);
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
transition: border-color 0.12s;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.pkg:hover {
|
|
157
|
+
border-color: var(--border-strong);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.pkg__header {
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
gap: 10px;
|
|
164
|
+
padding: 12px 16px;
|
|
165
|
+
border-bottom: 1px solid var(--border);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.pkg__name {
|
|
169
|
+
flex: 1;
|
|
170
|
+
min-width: 0;
|
|
171
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
172
|
+
font-size: 13px;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
text-overflow: ellipsis;
|
|
176
|
+
white-space: nowrap;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.pkg__count {
|
|
180
|
+
flex-shrink: 0;
|
|
181
|
+
min-width: 24px;
|
|
182
|
+
text-align: right;
|
|
183
|
+
font-size: 12px;
|
|
184
|
+
color: var(--text-muted);
|
|
185
|
+
font-variant-numeric: tabular-nums;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.pkg__list {
|
|
189
|
+
list-style: none;
|
|
190
|
+
margin: 0;
|
|
191
|
+
padding: 6px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.pkg__link {
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
gap: 12px;
|
|
198
|
+
padding: 10px 12px;
|
|
199
|
+
border-radius: var(--radius-sm);
|
|
200
|
+
text-decoration: none;
|
|
201
|
+
color: inherit;
|
|
202
|
+
transition: background 0.08s;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.pkg__link:hover {
|
|
206
|
+
background: var(--surface-hover);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.pkg__link:focus-visible {
|
|
210
|
+
outline: none;
|
|
211
|
+
background: var(--surface-hover);
|
|
212
|
+
box-shadow: inset 0 0 0 2px var(--border-strong);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.pkg__test-name {
|
|
216
|
+
flex: 1;
|
|
217
|
+
min-width: 0;
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
font-weight: 500;
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
text-overflow: ellipsis;
|
|
222
|
+
white-space: nowrap;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.pkg__test-slug {
|
|
226
|
+
flex-shrink: 0;
|
|
227
|
+
max-width: 50%;
|
|
228
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
229
|
+
font-size: 11px;
|
|
230
|
+
color: var(--text-subtle);
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
text-overflow: ellipsis;
|
|
233
|
+
white-space: nowrap;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@media (max-width: 600px) {
|
|
237
|
+
.app {
|
|
238
|
+
padding: 0 16px 48px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.app__header {
|
|
242
|
+
padding: 20px 0 16px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.app__list {
|
|
246
|
+
grid-template-columns: 1fr;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.pkg__test-slug {
|
|
250
|
+
display: none;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="data:image/png;base64,iVBORw0KGgo=">
|
|
7
|
+
<title>CKEditor 5 manual tests catalog</title>
|
|
8
|
+
<script type="module" src="./catalog.ts"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<template id="manual-index-template">
|
|
12
|
+
<main class="app">
|
|
13
|
+
<header class="app__header">
|
|
14
|
+
<div class="app__titles">
|
|
15
|
+
<h1 class="app__title">CKEditor 5 manual tests</h1>
|
|
16
|
+
<p class="app__subtitle"></p>
|
|
17
|
+
</div>
|
|
18
|
+
<label class="app__search">
|
|
19
|
+
<svg class="app__search-icon" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16">
|
|
20
|
+
<path fill="currentColor" d="M7 2a5 5 0 103.546 8.546l3.204 3.204a.75.75 0 101.06-1.06l-3.204-3.204A5 5 0 007 2zM3.5 7a3.5 3.5 0 117 0 3.5 3.5 0 01-7 0z"/>
|
|
21
|
+
</svg>
|
|
22
|
+
<input type="search" class="app__search-input" placeholder="Filter by package or test name…" autocomplete="off" autofocus>
|
|
23
|
+
</label>
|
|
24
|
+
</header>
|
|
25
|
+
<div class="app__list" role="list"></div>
|
|
26
|
+
</main>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<template id="manual-empty-template">
|
|
30
|
+
<div class="app__empty">
|
|
31
|
+
<p class="app__empty-title">No matches</p>
|
|
32
|
+
<p class="app__empty-hint">Try a different search term.</p>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<template id="manual-package-template">
|
|
37
|
+
<section class="pkg" role="listitem">
|
|
38
|
+
<header class="pkg__header">
|
|
39
|
+
<code class="pkg__name"></code>
|
|
40
|
+
<span class="pkg__count"></span>
|
|
41
|
+
</header>
|
|
42
|
+
<ul class="pkg__list"></ul>
|
|
43
|
+
</section>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<template id="manual-test-template">
|
|
47
|
+
<li class="pkg__item">
|
|
48
|
+
<a class="pkg__link">
|
|
49
|
+
<span class="pkg__test-name"></span>
|
|
50
|
+
<span class="pkg__test-slug"></span>
|
|
51
|
+
</a>
|
|
52
|
+
</li>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<div id="app"></div>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
package/theme/catalog.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// @ts-expect-error Virtual module provided by the manual test entries plugin.
|
|
7
|
+
import { manualTestEntries, type ManualTestEntry } from 'virtual:ckeditor5-manual-entries';
|
|
8
|
+
|
|
9
|
+
import './catalog.css';
|
|
10
|
+
|
|
11
|
+
const SEARCH_QUERY_PARAMETER = 'q';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Root element for the manual test index application.
|
|
15
|
+
*/
|
|
16
|
+
const appElement = document.querySelector<HTMLElement>( '#app' )!;
|
|
17
|
+
|
|
18
|
+
renderApp( appElement, manualTestEntries );
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Renders the index shell from the HTML template and wires filtering behavior.
|
|
22
|
+
*/
|
|
23
|
+
function renderApp( root: HTMLElement, entries: Array<ManualTestEntry> ): void {
|
|
24
|
+
const packageCount = new Set( entries.map( entry => entry.packageName ) ).size;
|
|
25
|
+
|
|
26
|
+
root.replaceChildren( cloneTemplateElement( 'manual-index-template' ) );
|
|
27
|
+
|
|
28
|
+
root.querySelector<HTMLElement>( '.app__subtitle' )!.textContent = `${ entries.length } tests across ${ packageCount } packages`;
|
|
29
|
+
|
|
30
|
+
const listElement = root.querySelector<HTMLElement>( '.app__list' )!;
|
|
31
|
+
const searchInput = root.querySelector<HTMLInputElement>( '.app__search-input' )!;
|
|
32
|
+
|
|
33
|
+
const render = ( query: string ): void => {
|
|
34
|
+
listElement.replaceChildren( ...renderGroups( filterEntries( entries, query ) ) );
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const setQuery = ( query: string ): void => {
|
|
38
|
+
searchInput.value = query;
|
|
39
|
+
render( query );
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
searchInput.addEventListener( 'input', () => {
|
|
43
|
+
updateSearchQueryInUrl( searchInput.value );
|
|
44
|
+
render( searchInput.value );
|
|
45
|
+
} );
|
|
46
|
+
|
|
47
|
+
window.addEventListener( 'popstate', () => {
|
|
48
|
+
setQuery( getSearchQueryFromUrl() );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
document.addEventListener( 'keydown', event => {
|
|
52
|
+
if ( event.key == '/' && document.activeElement != searchInput ) {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
searchInput.focus();
|
|
55
|
+
searchInput.select();
|
|
56
|
+
}
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
setQuery( getSearchQueryFromUrl() );
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns the current search query from the page URL.
|
|
64
|
+
*/
|
|
65
|
+
function getSearchQueryFromUrl(): string {
|
|
66
|
+
return new URL( window.location.href ).searchParams.get( SEARCH_QUERY_PARAMETER ) || '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Updates the page URL to reflect the current search query.
|
|
71
|
+
*/
|
|
72
|
+
function updateSearchQueryInUrl( query: string ): void {
|
|
73
|
+
if ( query == getSearchQueryFromUrl() ) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const url = new URL( window.location.href );
|
|
78
|
+
|
|
79
|
+
if ( query ) {
|
|
80
|
+
url.searchParams.set( SEARCH_QUERY_PARAMETER, query );
|
|
81
|
+
} else {
|
|
82
|
+
url.searchParams.delete( SEARCH_QUERY_PARAMETER );
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
history.replaceState( null, '', `${ url.pathname }${ url.search }${ url.hash }` );
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns entries matching the current search query.
|
|
90
|
+
*/
|
|
91
|
+
function filterEntries( entries: Array<ManualTestEntry>, query: string ): Array<ManualTestEntry> {
|
|
92
|
+
const normalized = query.trim().toLowerCase();
|
|
93
|
+
|
|
94
|
+
if ( !normalized ) {
|
|
95
|
+
return entries;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return entries.filter( entry =>
|
|
99
|
+
entry.packageName.toLowerCase().includes( normalized ) ||
|
|
100
|
+
entry.slug.toLowerCase().includes( normalized ) ||
|
|
101
|
+
entry.displayName.toLowerCase().includes( normalized )
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Groups visible entries by package and renders package cards.
|
|
107
|
+
*/
|
|
108
|
+
function renderGroups( entries: Array<ManualTestEntry> ): Array<HTMLElement> {
|
|
109
|
+
if ( !entries.length ) {
|
|
110
|
+
return [ cloneTemplateElement( 'manual-empty-template' ) ];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const groups = new Map<string, Array<ManualTestEntry>>();
|
|
114
|
+
|
|
115
|
+
for ( const entry of entries ) {
|
|
116
|
+
const group = groups.get( entry.packageName ) || [];
|
|
117
|
+
|
|
118
|
+
group.push( entry );
|
|
119
|
+
groups.set( entry.packageName, group );
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [ ...groups.entries() ].map( ( [ packageName, packageEntries ] ) => renderPackageCard( packageName, packageEntries ) );
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Renders a single package card with all tests belonging to that package.
|
|
127
|
+
*/
|
|
128
|
+
function renderPackageCard( packageName: string, entries: Array<ManualTestEntry> ): HTMLElement {
|
|
129
|
+
const packageCard = cloneTemplateElement( 'manual-package-template' );
|
|
130
|
+
const list = packageCard.querySelector<HTMLElement>( '.pkg__list' )!;
|
|
131
|
+
|
|
132
|
+
packageCard.querySelector<HTMLElement>( '.pkg__name' )!.textContent = packageName;
|
|
133
|
+
packageCard.querySelector<HTMLElement>( '.pkg__count' )!.textContent = String( entries.length );
|
|
134
|
+
list.replaceChildren( ...entries.map( entry => renderTestItem( entry ) ) );
|
|
135
|
+
|
|
136
|
+
return packageCard;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Renders a single test link inside a package card.
|
|
141
|
+
*/
|
|
142
|
+
function renderTestItem( entry: ManualTestEntry ): HTMLElement {
|
|
143
|
+
const testItem = cloneTemplateElement( 'manual-test-template' );
|
|
144
|
+
const link = testItem.querySelector<HTMLAnchorElement>( '.pkg__link' )!;
|
|
145
|
+
|
|
146
|
+
link.href = entry.href;
|
|
147
|
+
testItem.querySelector<HTMLElement>( '.pkg__test-name' )!.textContent = entry.displayName;
|
|
148
|
+
testItem.querySelector<HTMLElement>( '.pkg__test-slug' )!.textContent = entry.slug;
|
|
149
|
+
|
|
150
|
+
return testItem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Clones a one-root HTML template used by the index UI.
|
|
155
|
+
*/
|
|
156
|
+
function cloneTemplateElement( templateId: string ): HTMLElement {
|
|
157
|
+
const fragment = document
|
|
158
|
+
.querySelector<HTMLTemplateElement>( `#${ templateId }` )!
|
|
159
|
+
.content.cloneNode( true ) as DocumentFragment;
|
|
160
|
+
|
|
161
|
+
return fragment.firstElementChild as HTMLElement;
|
|
162
|
+
}
|