@bongos/core 1.19.492 → 1.19.494
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/.bongos-core.json +449 -34
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +151 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +106 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +26 -14
- package/docs/copy-registry.json +123 -15
- package/docs/design/reviews/hall-v2/README.md +124 -0
- package/docs/design/reviews/hall-v2/atlas--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/atlas--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/atlas--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/collab--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/collab--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/collab--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/copy-desk--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/copy-desk--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/copy-desk--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/diagrams--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/diagrams--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/diagrams--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/drachmae--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/drachmae--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/drachmae--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/gate--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/gate--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/gate--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/goals--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/goals--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/goals--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/government--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/government--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/government--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/harbor--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/harbor--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/harbor--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/idea--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/idea--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/idea--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/ideas--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/ideas--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/ideas--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/index--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/index--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/index--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/modules--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/modules--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/modules--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/not-ready--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/not-ready--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/not-ready--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/pair--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/pair--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/pair--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/people--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/people--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/people--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/primer--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/primer--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/primer--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/profile--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/profile--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/profile--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/ranks--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/ranks--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/ranks--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/roadmap--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/roadmap--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/roadmap--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/roster--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/roster--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/roster--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/sessions--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/sessions--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/sessions--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/settings--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/settings--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/settings--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/task--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/task--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/task--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/watch--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/watch--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/watch--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/work--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/work--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/work--xenos.webp +0 -0
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/copy-desk/module.json +2 -2
- package/modules/copy-desk/proposals.js +278 -0
- package/modules/copy-desk/routes/copy-desk.js +173 -9
- package/modules/copy-desk/tests/copy_no_cms.mjs +90 -12
- package/modules/copy-desk/tests/copy_proposals.mjs +279 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/copy-desk.css +9 -1
- package/modules/hall-ui/public/copy-desk.html +33 -0
- package/modules/hall-ui/public/copy-desk.js +134 -3
- package/modules/hall-ui/public/copy-desk.states.json +18 -1
- package/modules/lifecycle/lifecycle.js +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/copy-apply.js +338 -0
- package/scripts/gds/copy-inventory.js +128 -30
- package/src/module-api.js +1 -1
- package/tests/hall_collab_world.mjs +24 -4
|
@@ -132,6 +132,28 @@ function looksLikeCopy(raw) {
|
|
|
132
132
|
return true;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// THE SPAN: where a row's text physically SITS in the file, as [start, end)
|
|
136
|
+
// character offsets. Every extractor below records one alongside the text.
|
|
137
|
+
//
|
|
138
|
+
// It is deliberately NOT part of the committed registry — buildRegistry drops it
|
|
139
|
+
// (see the note there). A committed offset would go stale on the next edit to an
|
|
140
|
+
// unrelated line of the same file, and the registry's whole point is an id that
|
|
141
|
+
// survives movement. The span is a LIVE fact, recomputed from the tree by whoever
|
|
142
|
+
// needs it: today that is scripts/gds/copy-apply.js (R03 of goal 1000074), which
|
|
143
|
+
// re-runs this extractor against the working tree and uses the span to replace
|
|
144
|
+
// exactly the characters a string occupies and nothing else. Deriving it here,
|
|
145
|
+
// beside the regex that captured the text, is what keeps the applier from owning
|
|
146
|
+
// a second, drifting copy of these matching rules.
|
|
147
|
+
//
|
|
148
|
+
// The span covers the TRIMMED text — a row's `text` is whitespace-collapsed and
|
|
149
|
+
// trimmed, so the indentation around a multi-line literal belongs to the file's
|
|
150
|
+
// formatting, not to the copy, and an applier must leave it alone.
|
|
151
|
+
function trimmedSpan(group, groupStart) {
|
|
152
|
+
const lead = group.length - group.replace(/^\s+/, '').length;
|
|
153
|
+
const trail = group.length - group.replace(/\s+$/, '').length;
|
|
154
|
+
return { start: groupStart + lead, end: groupStart + group.length - trail };
|
|
155
|
+
}
|
|
156
|
+
|
|
135
157
|
// Strip <script>/<style> bodies so their contents are not read as page text.
|
|
136
158
|
// Replaced with spaces of equal length so every line number stays truthful.
|
|
137
159
|
function stripEmbedded(html) {
|
|
@@ -151,7 +173,8 @@ function fromHtml({ source, rel }) {
|
|
|
151
173
|
const textRe = />([^<>]+)</g;
|
|
152
174
|
while ((m = textRe.exec(cleaned)) !== null) {
|
|
153
175
|
if (!looksLikeCopy(m[1])) continue;
|
|
154
|
-
|
|
176
|
+
// The captured group opens one character past the '>' that anchored the match.
|
|
177
|
+
out.push({ text: m[1].replace(/\s+/g, ' ').trim(), file: rel, line: lineAt(starts, m.index), origin: 'html-text', confidence: 'certain', ...trimmedSpan(m[1], m.index + 1) });
|
|
155
178
|
}
|
|
156
179
|
|
|
157
180
|
// 2. copy-bearing attributes.
|
|
@@ -159,14 +182,19 @@ function fromHtml({ source, rel }) {
|
|
|
159
182
|
re.lastIndex = 0;
|
|
160
183
|
while ((m = re.exec(cleaned)) !== null) {
|
|
161
184
|
if (!looksLikeCopy(m[1])) continue;
|
|
162
|
-
|
|
185
|
+
// Every COPY_ATTR_RE ends with the closing quote, so the group's last
|
|
186
|
+
// character sits exactly one before the match's end. DERIVED, never
|
|
187
|
+
// searched for — indexOf would land on an earlier copy of the same text.
|
|
188
|
+
const attrStart = m.index + m[0].length - 1 - m[1].length;
|
|
189
|
+
out.push({ text: m[1].replace(/\s+/g, ' ').trim(), file: rel, line: lineAt(starts, m.index), origin: `html-attr:${attr}`, confidence: 'certain', ...trimmedSpan(m[1], attrStart) });
|
|
163
190
|
}
|
|
164
191
|
}
|
|
165
192
|
|
|
166
193
|
// 3. <title> is the page's own name — always copy.
|
|
167
194
|
const titleRe = /<title>([^<]{2,200})<\/title>/gi;
|
|
168
195
|
while ((m = titleRe.exec(source)) !== null) {
|
|
169
|
-
|
|
196
|
+
const titleStart = m.index + '<title>'.length;
|
|
197
|
+
out.push({ text: m[1].replace(/\s+/g, ' ').trim(), file: rel, line: lineAt(starts, m.index), origin: 'html-title', confidence: 'certain', ...trimmedSpan(m[1], titleStart) });
|
|
170
198
|
}
|
|
171
199
|
return out;
|
|
172
200
|
}
|
|
@@ -198,24 +226,75 @@ function unescapeLiteral(raw) {
|
|
|
198
226
|
return String(raw).replace(/\\(['"`\\])/g, '$1');
|
|
199
227
|
}
|
|
200
228
|
|
|
201
|
-
//
|
|
229
|
+
// Can offsets into this match's BODY be read as offsets into the file?
|
|
230
|
+
//
|
|
231
|
+
// A template literal (group 1) is used raw, so yes. A quoted literal is
|
|
232
|
+
// `unescapeLiteral`d first, and that SHIFTS every character after a backslash —
|
|
233
|
+
// so yes only when the unescape changed nothing. The alternative, an
|
|
234
|
+
// escape-aware index map, would buy spans for a handful of quoted-markup rows
|
|
235
|
+
// and add a second place for the escaping rules to be wrong. A row with no span
|
|
236
|
+
// is refused BY NAME in copy-apply.js instead, which is the honest trade.
|
|
237
|
+
function literalIsUnshifted(m) {
|
|
238
|
+
if (m[1] !== undefined) return true;
|
|
239
|
+
const raw = m[2] ?? m[3];
|
|
240
|
+
return raw === undefined ? false : unescapeLiteral(raw) === raw;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Interpolations are replaced by a fixed-width PLACEHOLDER before matching, so a
|
|
244
|
+
// position in the holed string is NOT a position in the source. This records the
|
|
245
|
+
// literal runs between the holes so one can be converted back to the other —
|
|
246
|
+
// which is what lets a markup row carry a source span (see `trimmedSpan`).
|
|
247
|
+
//
|
|
248
|
+
// The conversion is only ever asked for at a capture BOUNDARY, and every boundary
|
|
249
|
+
// sits on a real character: a text node's group opens just past a '>' and closes
|
|
250
|
+
// just before a '<', and neither can appear inside `{…}`. A boundary that lands
|
|
251
|
+
// where a hole meets the next run resolves to the character after the closing
|
|
252
|
+
// '}' of the interpolation, which is the right place to cut.
|
|
253
|
+
function holeMap(body) {
|
|
254
|
+
const re = /\$\{[^{}]*\}/g;
|
|
255
|
+
const runs = [];
|
|
256
|
+
let withHoles = '';
|
|
257
|
+
let last = 0;
|
|
258
|
+
let m;
|
|
259
|
+
while ((m = re.exec(body)) !== null) {
|
|
260
|
+
const literal = body.slice(last, m.index);
|
|
261
|
+
runs.push({ holed: withHoles.length, raw: last, length: literal.length });
|
|
262
|
+
withHoles += literal + PLACEHOLDER;
|
|
263
|
+
last = m.index + m[0].length;
|
|
264
|
+
}
|
|
265
|
+
const tail = body.slice(last);
|
|
266
|
+
runs.push({ holed: withHoles.length, raw: last, length: tail.length });
|
|
267
|
+
return { withHoles: withHoles + tail, runs };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function holedToRaw(runs, at) {
|
|
271
|
+
for (const run of runs) {
|
|
272
|
+
if (at >= run.holed && at <= run.holed + run.length) return run.raw + (at - run.holed);
|
|
273
|
+
}
|
|
274
|
+
return null; // only reachable for a position strictly inside a placeholder
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// A literal that carries markup: extract what a person reads out of it, each row
|
|
278
|
+
// carrying [start, end) offsets into the RAW body it was found in.
|
|
202
279
|
function copyFromMarkupLiteral(body) {
|
|
203
|
-
const withHoles = body
|
|
280
|
+
const { withHoles, runs } = holeMap(body);
|
|
204
281
|
const out = [];
|
|
282
|
+
const emit = (group, groupStart) => {
|
|
283
|
+
const t = group.replace(/\s+/g, ' ').trim();
|
|
284
|
+
if (t === PLACEHOLDER || !looksLikeCopy(t.replace(/\{…\}/g, 'x'))) return;
|
|
285
|
+
const span = trimmedSpan(group, groupStart);
|
|
286
|
+
const start = holedToRaw(runs, span.start);
|
|
287
|
+
const end = holedToRaw(runs, span.end);
|
|
288
|
+
// A span that cannot be mapped back is reported as text with no span rather
|
|
289
|
+
// than as a guess; the applier refuses such a row by name.
|
|
290
|
+
out.push(start === null || end === null ? { text: t } : { text: t, start, end });
|
|
291
|
+
};
|
|
205
292
|
let m;
|
|
206
293
|
const textRe = />([^<>]+)</g;
|
|
207
|
-
while ((m = textRe.exec(withHoles)) !== null)
|
|
208
|
-
const t = m[1].replace(/\s+/g, ' ').trim();
|
|
209
|
-
if (t === PLACEHOLDER || !looksLikeCopy(t.replace(/\{…\}/g, 'x'))) continue;
|
|
210
|
-
out.push(t);
|
|
211
|
-
}
|
|
294
|
+
while ((m = textRe.exec(withHoles)) !== null) emit(m[1], m.index + 1);
|
|
212
295
|
for (const { re } of COPY_ATTR_RES) {
|
|
213
296
|
re.lastIndex = 0;
|
|
214
|
-
while ((m = re.exec(withHoles)) !== null)
|
|
215
|
-
const t = m[1].replace(/\s+/g, ' ').trim();
|
|
216
|
-
if (!looksLikeCopy(t.replace(/\{…\}/g, 'x'))) continue;
|
|
217
|
-
out.push(t);
|
|
218
|
-
}
|
|
297
|
+
while ((m = re.exec(withHoles)) !== null) emit(m[1], m.index + m[0].length - 1 - m[1].length);
|
|
219
298
|
}
|
|
220
299
|
return out;
|
|
221
300
|
}
|
|
@@ -224,12 +303,12 @@ function fromJs({ source, rel }) {
|
|
|
224
303
|
const out = [];
|
|
225
304
|
const starts = lineIndex(source);
|
|
226
305
|
const seen = new Set(); // (text, line) — one row per string per site
|
|
227
|
-
const push = (text, index, origin) => {
|
|
306
|
+
const push = (text, index, origin, span = null) => {
|
|
228
307
|
const line = lineAt(starts, index);
|
|
229
308
|
const key = `${text}\u0000${line}\u0000${origin}`;
|
|
230
309
|
if (seen.has(key)) return;
|
|
231
310
|
seen.add(key);
|
|
232
|
-
out.push({ text, file: rel, line, origin, confidence: 'certain' });
|
|
311
|
+
out.push({ text, file: rel, line, origin, confidence: 'certain', ...(span || {}) });
|
|
233
312
|
};
|
|
234
313
|
|
|
235
314
|
let m;
|
|
@@ -238,19 +317,31 @@ function fromJs({ source, rel }) {
|
|
|
238
317
|
while ((m = JS_LITERALS.exec(source)) !== null) {
|
|
239
318
|
const body = m[1] ?? unescapeLiteral(m[2] ?? m[3] ?? '');
|
|
240
319
|
if (!body || !/<[a-zA-Z/]/.test(body) || !/>/.test(body)) continue;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
while ((m = TEXT_ASSIGN.exec(source)) !== null) {
|
|
251
|
-
const body = (m[1] ?? unescapeLiteral(m[2] ?? m[3] ?? '')).replace(/\$\{[^{}]*\}/g, PLACEHOLDER).replace(/\s+/g, ' ').trim();
|
|
252
|
-
if (body && looksLikeCopy(body.replace(/\{…\}/g, 'x'))) push(body, m.index, 'js-text-assign');
|
|
320
|
+
// Each alternative opens with exactly one delimiter, so the body starts one
|
|
321
|
+
// character past the match.
|
|
322
|
+
const mapped = literalIsUnshifted(m);
|
|
323
|
+
for (const f of copyFromMarkupLiteral(body)) {
|
|
324
|
+
const span = mapped && f.start !== undefined
|
|
325
|
+
? { start: m.index + 1 + f.start, end: m.index + 1 + f.end }
|
|
326
|
+
: null;
|
|
327
|
+
push(f.text, m.index, 'js-markup', span);
|
|
328
|
+
}
|
|
253
329
|
}
|
|
330
|
+
// 2. strings shown directly to a person. Both of these normalize the WHOLE
|
|
331
|
+
// literal into the row's text, so the span is the whole body: the delimiter
|
|
332
|
+
// closes the match, so the body ends one character before the match does.
|
|
333
|
+
const wholeLiteral = (re, origin) => {
|
|
334
|
+
re.lastIndex = 0;
|
|
335
|
+
while ((m = re.exec(source)) !== null) {
|
|
336
|
+
const rawBody = m[1] ?? m[2] ?? m[3] ?? '';
|
|
337
|
+
const body = (m[1] ?? unescapeLiteral(m[2] ?? m[3] ?? '')).replace(/\$\{[^{}]*\}/g, PLACEHOLDER).replace(/\s+/g, ' ').trim();
|
|
338
|
+
if (!body || !looksLikeCopy(body.replace(/\{…\}/g, 'x'))) continue;
|
|
339
|
+
const bodyStart = m.index + m[0].length - 1 - rawBody.length;
|
|
340
|
+
push(body, m.index, origin, literalIsUnshifted(m) ? trimmedSpan(rawBody, bodyStart) : null);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
wholeLiteral(UI_CALL, 'js-notice');
|
|
344
|
+
wholeLiteral(TEXT_ASSIGN, 'js-text-assign');
|
|
254
345
|
return out;
|
|
255
346
|
}
|
|
256
347
|
|
|
@@ -276,7 +367,14 @@ function buildRegistry() {
|
|
|
276
367
|
? fromHtml({ source, rel })
|
|
277
368
|
: fromJs({ source, rel });
|
|
278
369
|
for (const f of found) {
|
|
279
|
-
|
|
370
|
+
// THE SPAN IS DROPPED HERE, DELIBERATELY. Every extractor records where a
|
|
371
|
+
// string physically sits (see `trimmedSpan`), and none of it is committed:
|
|
372
|
+
// an offset goes stale the moment anything above it in the file changes,
|
|
373
|
+
// so a registry carrying one would be wrong far more often than the id —
|
|
374
|
+
// which is designed to survive exactly that movement — ever is. The span
|
|
375
|
+
// is recomputed from the tree by the applier, at the moment it edits.
|
|
376
|
+
const { start, end, ...row } = f;
|
|
377
|
+
entries.push({ id: stableId(surface.id, f.text), surface: surface.id, ...row });
|
|
280
378
|
count += 1;
|
|
281
379
|
}
|
|
282
380
|
}
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.494'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -158,13 +158,32 @@ test('the filters and the search are the kit\'s bar; the duplicates are paged th
|
|
|
158
158
|
assert.match(read('copy-desk.html'), /src="\/builders\/hall-kit\.js/, 'the page loads the kit');
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
test('the
|
|
161
|
+
test('the three dialogs are the family modal on a native <dialog>, with the number input in a field', () => {
|
|
162
162
|
const html = read('copy-desk.html');
|
|
163
|
-
|
|
163
|
+
// Three since task 1003114 (R03): flag, close, and the proposal composer.
|
|
164
|
+
assert.equal((html.match(/<dialog id="cd-(close-dialog|dialog|propose-dialog)" class="ov-modal cd-dialog">/g) || []).length, 3);
|
|
164
165
|
assert.match(html, /<div class="ov-field ov-field--wide">\s*<label class="ov-field__l" for="cd-close-task">[^<]*<\/label>\s*<input type="number" id="cd-close-task"/, 'the number input sits in the family field, which restates the 30px pill');
|
|
165
166
|
assert.ok(!/<label[^>]*>[^<]*<span class="cd-dialog__hint"/.test(html), 'no hint lives inside a label (the label voice would uppercase it)');
|
|
166
|
-
assert.equal((html.match(/class="ov-form__why"/g) || []).length,
|
|
167
|
+
assert.equal((html.match(/class="ov-form__why"/g) || []).length, 4, 'the four hints are the form\'s own explainer line');
|
|
167
168
|
assert.match(html, /class="btn-accent" id="cd-submit"/, 'the accent door files the flag');
|
|
169
|
+
assert.match(html, /class="btn-accent" id="cd-propose-submit"/, 'and the proposal composer has its own');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('the proposal composer is prefilled, and says what filing it does and does not do', () => {
|
|
173
|
+
const html = read('copy-desk.html');
|
|
174
|
+
const js = read('copy-desk.js');
|
|
175
|
+
// The composer opens with the CURRENT wording in the box — an artist edits a
|
|
176
|
+
// sentence rather than retyping it out of the quote above, which is how a
|
|
177
|
+
// proposal accidentally changes a word nobody meant to change.
|
|
178
|
+
assert.match(js, /\$\('#cd-propose-text'\)\.value = t\.text;/, 'the textarea opens prefilled with the current wording');
|
|
179
|
+
// The placeholder rule is shown only when the sentence has holes, because for
|
|
180
|
+
// most strings it is noise and for a few it is the reason the change refuses.
|
|
181
|
+
assert.match(html, /id="cd-propose-holes" hidden/, 'the placeholder rule starts hidden');
|
|
182
|
+
assert.match(js, /hint\.hidden = n === 0;/, 'and is shown only for a sentence with holes');
|
|
183
|
+
// NO LIVE CMS, said to the person in the dialog rather than only in an ADR.
|
|
184
|
+
assert.match(html, /Nothing changes yet\. This files a task carrying your wording/,
|
|
185
|
+
'the composer tells the artist their words do not reach a reader until a task ships');
|
|
186
|
+
assert.match(js, /copy-desk\/proposals/, 'and it posts to the proposal route');
|
|
168
187
|
});
|
|
169
188
|
|
|
170
189
|
test('the copy desk renders MORE facts per row than any other page, and lost none', () => {
|
|
@@ -205,10 +224,11 @@ test('the harness fixtures exist and carry the shapes the pages read', () => {
|
|
|
205
224
|
|
|
206
225
|
test('the states files walk the new states', () => {
|
|
207
226
|
const cd = JSON.parse(read('copy-desk.states.json'));
|
|
208
|
-
assert.deepEqual(Object.keys(cd.states), ['queue', 'out', 'search', 'composer', 'closer']);
|
|
227
|
+
assert.deepEqual(Object.keys(cd.states), ['queue', 'out', 'search', 'composer', 'closer', 'proposer']);
|
|
209
228
|
assert.deepEqual(cd.states.search.actions[1], ['fill', '#cd-search', 'save']);
|
|
210
229
|
assert.deepEqual(cd.states.composer.actions[1], ['clickIfVisible', '.cd-flag-btn'], 'clickIfVisible: against the kit\'s stub the button does not exist');
|
|
211
230
|
assert.deepEqual(cd.states.closer.actions[1], ['clickIfVisible', '.cd-close-btn']);
|
|
231
|
+
assert.deepEqual(cd.states.proposer.actions[1], ['clickIfVisible', '.cd-propose-btn'], 'task 1003114: the proposal composer is a measured state, not one looked at once');
|
|
212
232
|
const cb = JSON.parse(read('collab.states.json'));
|
|
213
233
|
assert.deepEqual(cb.states.board.expect.visible, ['#cb-for-me']);
|
|
214
234
|
});
|